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_chart_release.yml b/.github/workflows/helm-release-infisical-core.yml similarity index 82% rename from .github/workflows/helm_chart_release.yml rename to .github/workflows/helm-release-infisical-core.yml index 8f47da69d..0588d5d0d 100644 --- a/.github/workflows/helm_chart_release.yml +++ b/.github/workflows/helm-release-infisical-core.yml @@ -1,4 +1,4 @@ -name: Release Helm Charts +name: Release Infisical Core Helm chart on: [workflow_dispatch] @@ -17,6 +17,6 @@ jobs: - 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 + 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/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_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 3a3b384f3..c592daf81 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -26,7 +26,7 @@ jobs: CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} npm-release: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: working-directory: ./npm needs: @@ -83,7 +83,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} goreleaser: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest needs: [cli-integration-tests] steps: - uses: actions/checkout@v3 @@ -103,11 +103,12 @@ jobs: go-version: ">=1.19.3" cache: true cache-dependency-path: cli/go.sum - - name: libssl1.1 => libssl1.0-dev for OSXCross + - 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 update && apt-cache policy libssl1.0-dev - sudo apt-get install libssl1.0-dev + 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 diff --git a/.github/workflows/release_docker_k8_operator.yaml b/.github/workflows/release_docker_k8_operator.yaml index 517549ea8..4af62df1c 100644 --- a/.github/workflows/release_docker_k8_operator.yaml +++ b/.github/workflows/release_docker_k8_operator.yaml @@ -1,37 +1,103 @@ -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*.*.*" 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/.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/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index dfcb87deb..33360bf45 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -8,7 +8,7 @@ FROM node:20-slim AS base FROM base AS frontend-dependencies 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 @@ -23,17 +23,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 @@ -44,20 +43,10 @@ WORKDIR /app RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g 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 ## @@ -69,13 +58,21 @@ RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user WORKDIR /app -# Required for pkcs11js +# 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 @@ -91,13 +88,21 @@ ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ WORKDIR /app -# Required for pkcs11js +# 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 @@ -108,13 +113,25 @@ RUN mkdir frontend-build # Production stage FROM base AS production -# Install necessary packages +# 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 \ @@ -132,14 +149,11 @@ 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 +ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY WORKDIR / @@ -147,6 +161,9 @@ 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 @@ -164,4 +181,4 @@ EXPOSE 443 USER non-root-user -CMD ["./standalone-entrypoint.sh"] \ No newline at end of file +CMD ["./standalone-entrypoint.sh"] diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 41c898b79..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,34 +42,34 @@ 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 -# Required for pkcs11js -RUN apk add --no-cache python3 make g++ +# 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 @@ -88,8 +84,20 @@ FROM base AS backend-runner WORKDIR /app -# Required for pkcs11js -RUN apk add --no-cache python3 make g++ +# 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 @@ -100,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 @@ -118,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 @@ -140,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..e79517cf8 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,6 @@ Hiring (Remote/SF) -

- - - - - Deploy to DO - -

-

Infisical is released under the MIT license. @@ -65,7 +56,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus - **[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 582264946..0edfdfb84 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,13 +1,22 @@ # Build stage -FROM node:20-alpine AS build +FROM node:20-slim AS build WORKDIR /app # Required for pkcs11js -RUN apk --update add \ - python3 \ - make \ - g++ +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + openssh-client + +# 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 @@ -16,25 +25,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 apk --update add \ - python3 \ - make \ - g++ +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 97bc2c6a3..adb5157f5 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:20-slim # ? Setup a test SoftHSM module. In production a real HSM is used. @@ -7,19 +7,32 @@ ARG SOFTHSM2_VERSION=2.5.0 ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \ SOFTHSM2_SOURCES=/tmp/softhsm2 -# install build dependencies including python3 -RUN apk --update add \ - alpine-sdk \ - autoconf \ - automake \ - git \ - libtool \ - openssl-dev \ - python3 \ - make \ - g++ +# 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 -# build and install SoftHSM2 +# 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} @@ -32,16 +45,18 @@ RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \ WORKDIR /root RUN rm -fr ${SOFTHSM2_SOURCES} -# install pkcs11-tool -RUN apk --update add opensc +# Install pkcs11-tool +RUN apt-get install -y opensc -RUN softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 +RUN mkdir -p /etc/softhsm2/tokens && \ + softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 # ? App setup -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 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/e2e-test/mocks/queue.ts b/backend/e2e-test/mocks/queue.ts index c694979db..99e3999e1 100644 --- a/backend/e2e-test/mocks/queue.ts +++ b/backend/e2e-test/mocks/queue.ts @@ -10,17 +10,22 @@ export const mockQueue = (): TQueueServiceFactory => { queue: async (name, jobData) => { job[name] = jobData; }, + queuePg: 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 866b0f45f..46b322349 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -23,14 +23,14 @@ 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 { @@ -42,6 +42,7 @@ export default { }, true ); + await db.migrate.latest({ directory: path.join(__dirname, "../src/db/migrations"), extension: "ts", @@ -52,14 +53,24 @@ 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 hsmModule = initializeHsmModule(); + const smtp = mockSmtpServer(); + 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() }); + const server = await main({ + db, + smtp, + logger, + queue, + keyStore, + hsmModule: hsmModule.getModule(), + redis, + envConfig + }); // @ts-expect-error type globalThis.testServer = server; @@ -73,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/package-lock.json b/backend/package-lock.json index d55521f5b..47e9014ce 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,16 +84,19 @@ "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", @@ -184,6 +201,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", @@ -4854,6 +4888,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", @@ -4865,6 +5004,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", @@ -5393,6 +5635,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" } @@ -5408,13 +5651,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", @@ -5487,19 +5727,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", @@ -5519,10 +5781,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", @@ -5541,16 +5813,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": { @@ -5577,6 +5918,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", @@ -5643,12 +6010,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": { @@ -5712,6 +6191,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", @@ -5754,6 +6339,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", @@ -5829,6 +6422,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", @@ -5907,9 +6509,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" } @@ -5966,6 +6569,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", @@ -6065,32 +6747,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" @@ -6101,25 +6786,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": { @@ -6806,15 +7509,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", @@ -7078,6 +8109,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", @@ -7451,6 +8536,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" }, @@ -7460,12 +8546,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", @@ -7477,24 +8564,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", @@ -7512,6 +8601,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" }, @@ -8450,18 +9540,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", @@ -8549,6 +9627,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": "*" } @@ -8668,9 +9747,10 @@ "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", @@ -8850,9 +9930,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", @@ -8909,7 +9990,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", @@ -8942,6 +10024,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", @@ -8961,6 +10048,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", @@ -8989,19 +10082,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": "*" } @@ -9010,6 +10095,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": "*" } @@ -9453,10 +10539,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" } @@ -9705,7 +10801,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" }, @@ -9713,6 +10808,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", @@ -10332,9 +11435,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", @@ -10702,14 +11806,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", @@ -10785,6 +11881,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", @@ -10921,6 +12029,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", @@ -11252,9 +12365,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", @@ -11264,6 +12377,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", @@ -11272,6 +12445,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", @@ -11281,6 +12529,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", @@ -11373,6 +12626,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", @@ -11425,6 +12689,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", @@ -11467,6 +12739,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", @@ -11478,6 +12758,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", @@ -11569,6 +12862,17 @@ "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/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -11631,13 +12935,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" } @@ -11651,15 +12951,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" @@ -12292,7 +13603,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", @@ -12332,9 +13644,9 @@ } }, "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", @@ -12356,7 +13668,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", @@ -12371,6 +13683,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-session": { @@ -12484,6 +13800,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", @@ -12905,12 +14226,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": { @@ -12925,6 +14247,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", @@ -13296,16 +14623,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" @@ -13322,6 +14653,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", @@ -13519,6 +14862,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", @@ -13548,11 +14929,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" @@ -13569,14 +14950,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", @@ -13670,6 +15043,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" }, @@ -13678,9 +15052,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" }, @@ -13689,11 +15063,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" @@ -13734,9 +15108,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" }, @@ -13819,6 +15193,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", @@ -13970,7 +15355,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" } @@ -14003,6 +15387,17 @@ "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", @@ -14122,6 +15517,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "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", @@ -14258,7 +15659,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", @@ -14359,6 +15761,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", @@ -14504,6 +15911,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", @@ -14522,6 +15949,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", @@ -14573,6 +16011,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", @@ -14870,6 +16414,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", @@ -15111,6 +16663,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", @@ -15186,8 +16743,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", @@ -15303,6 +16859,14 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "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", @@ -15528,6 +17092,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", @@ -15785,15 +17354,16 @@ } }, "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" }, @@ -15873,6 +17443,16 @@ } } }, + "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", @@ -16010,6 +17590,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", @@ -16302,6 +17887,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", @@ -16405,6 +18011,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", @@ -16453,10 +18067,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" } @@ -16503,6 +18128,7 @@ "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" @@ -16517,12 +18143,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" @@ -16546,6 +18174,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" }, @@ -16567,11 +18196,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", @@ -16605,6 +18229,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", @@ -16614,9 +18249,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", @@ -16755,9 +18391,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": { @@ -16790,15 +18426,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" }, @@ -16817,6 +18451,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", @@ -16854,17 +18501,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", @@ -16893,9 +18540,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", @@ -17317,6 +18964,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", @@ -17607,6 +19259,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", @@ -18002,7 +19689,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" } @@ -18015,6 +19701,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", @@ -18105,9 +19820,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", @@ -18218,6 +19933,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", @@ -18332,6 +20052,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", @@ -18365,6 +20096,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", @@ -18444,6 +20180,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", @@ -18536,6 +20286,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", @@ -19311,6 +21077,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", @@ -19553,6 +21324,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", @@ -19593,6 +21372,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", @@ -19663,6 +21463,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", @@ -19698,6 +21509,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", @@ -20392,6 +22212,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", @@ -20450,7 +22278,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" }, @@ -20733,6 +22560,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", @@ -21539,11 +23372,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", @@ -21836,47 +23699,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", @@ -21901,14 +23794,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" } @@ -21925,7 +23825,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 240b7db2c..c575722fd 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": [], @@ -127,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", @@ -156,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", @@ -164,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", @@ -176,16 +201,19 @@ "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", 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 ebe3f4289..5d10830ad 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"; @@ -29,9 +35,12 @@ import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-ap import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-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 { 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"; @@ -50,6 +59,7 @@ import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-acces 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"; @@ -74,18 +84,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 @@ -108,12 +137,18 @@ declare module "fastify" { isUserCompleted: string; providerAuthToken: string; }; + kmipUser: { + projectId: string; + clientId: string; + name: string; + }; auditLogInfo: Pick; ssoConfig: Awaited>; ldapConfig: Awaited>; } interface FastifyInstance { + redis: Redis; services: { login: TAuthLoginFactory; password: TAuthPasswordFactory; @@ -154,6 +189,7 @@ declare module "fastify" { identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAzureAuth: TIdentityAzureAuthServiceFactory; identityOidcAuth: TIdentityOidcAuthServiceFactory; + identityJwtAuth: TIdentityJwtAuthServiceFactory; accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; accessApprovalRequest: TAccessApprovalRequestServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; @@ -167,6 +203,8 @@ declare module "fastify" { auditLogStream: TAuditLogStreamServiceFactory; certificate: TCertificateServiceFactory; certificateTemplate: TCertificateTemplateServiceFactory; + sshCertificateAuthority: TSshCertificateAuthorityServiceFactory; + sshCertificateTemplate: TSshCertificateTemplateServiceFactory; certificateAuthority: TCertificateAuthorityServiceFactory; certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory; certificateEst: TCertificateEstServiceFactory; @@ -193,11 +231,18 @@ declare module "fastify" { migration: TExternalMigrationServiceFactory; externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory; projectTemplate: TProjectTemplateServiceFactory; + totp: TTotpServiceFactory; + appConnection: TAppConnectionServiceFactory; + secretSync: TSecretSyncServiceFactory; + kmip: TKmipServiceFactory; + kmipOperation: TKmipOperationServiceFactory; + gateway: TGatewayServiceFactory; }; // 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..346b08e4c 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -68,6 +68,9 @@ import { TExternalKms, TExternalKmsInsert, TExternalKmsUpdate, + TGateways, + TGatewaysInsert, + TGatewaysUpdate, TGitAppInstallSessions, TGitAppInstallSessionsInsert, TGitAppInstallSessionsUpdate, @@ -98,6 +101,9 @@ import { TIdentityGcpAuths, TIdentityGcpAuthsInsert, TIdentityGcpAuthsUpdate, + TIdentityJwtAuths, + TIdentityJwtAuthsInsert, + TIdentityJwtAuthsUpdate, TIdentityKubernetesAuths, TIdentityKubernetesAuthsInsert, TIdentityKubernetesAuthsUpdate, @@ -140,6 +146,18 @@ import { TInternalKms, TInternalKmsInsert, TInternalKmsUpdate, + TKmipClientCertificates, + TKmipClientCertificatesInsert, + TKmipClientCertificatesUpdate, + TKmipClients, + TKmipClientsInsert, + TKmipClientsUpdate, + TKmipOrgConfigs, + TKmipOrgConfigsInsert, + TKmipOrgConfigsUpdate, + TKmipOrgServerCertificates, + TKmipOrgServerCertificatesInsert, + TKmipOrgServerCertificatesUpdate, TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate, @@ -164,6 +182,9 @@ import { TOrgBots, TOrgBotsInsert, TOrgBotsUpdate, + TOrgGatewayConfig, + TOrgGatewayConfigInsert, + TOrgGatewayConfigUpdate, TOrgMemberships, TOrgMembershipsInsert, TOrgMembershipsUpdate, @@ -185,6 +206,9 @@ import { TProjectEnvironments, TProjectEnvironmentsInsert, TProjectEnvironmentsUpdate, + TProjectGateways, + TProjectGatewaysInsert, + TProjectGatewaysUpdate, TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate, @@ -199,6 +223,9 @@ import { TProjectSlackConfigs, TProjectSlackConfigsInsert, TProjectSlackConfigsUpdate, + TProjectSplitBackfillIds, + TProjectSplitBackfillIdsInsert, + TProjectSplitBackfillIdsUpdate, TProjectsUpdate, TProjectTemplates, TProjectTemplatesInsert, @@ -212,6 +239,9 @@ import { TRateLimit, TRateLimitInsert, TRateLimitUpdate, + TResourceMetadata, + TResourceMetadataInsert, + TResourceMetadataUpdate, TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate, @@ -311,9 +341,27 @@ import { TSlackIntegrations, TSlackIntegrationsInsert, TSlackIntegrationsUpdate, + TSshCertificateAuthorities, + TSshCertificateAuthoritiesInsert, + TSshCertificateAuthoritiesUpdate, + TSshCertificateAuthoritySecrets, + TSshCertificateAuthoritySecretsInsert, + TSshCertificateAuthoritySecretsUpdate, + TSshCertificateBodies, + TSshCertificateBodiesInsert, + TSshCertificateBodiesUpdate, + TSshCertificates, + TSshCertificatesInsert, + TSshCertificatesUpdate, + TSshCertificateTemplates, + TSshCertificateTemplatesInsert, + TSshCertificateTemplatesUpdate, TSuperAdmin, TSuperAdminInsert, TSuperAdminUpdate, + TTotpConfigs, + TTotpConfigsInsert, + TTotpConfigsUpdate, TTrustedIps, TTrustedIpsInsert, TTrustedIpsUpdate, @@ -339,11 +387,13 @@ import { TWorkflowIntegrationsInsert, TWorkflowIntegrationsUpdate } from "@app/db/schemas"; +import { TAppConnections, TAppConnectionsInsert, TAppConnectionsUpdate } from "@app/db/schemas/app-connections"; import { TExternalGroupOrgRoleMappings, TExternalGroupOrgRoleMappingsInsert, TExternalGroupOrgRoleMappingsUpdate } from "@app/db/schemas/external-group-org-role-mappings"; +import { TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate } from "@app/db/schemas/secret-syncs"; import { TSecretV2TagJunction, TSecretV2TagJunctionInsert, @@ -369,6 +419,31 @@ declare module "knex/types/tables" { interface Tables { [TableName.Users]: KnexOriginal.CompositeTableType; [TableName.Groups]: 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.CertificateAuthority]: KnexOriginal.CompositeTableType< TCertificateAuthorities, TCertificateAuthoritiesInsert, @@ -587,6 +662,11 @@ declare module "knex/types/tables" { TIdentityOidcAuthsInsert, TIdentityOidcAuthsUpdate >; + [TableName.IdentityJwtAuth]: KnexOriginal.CompositeTableType< + TIdentityJwtAuths, + TIdentityJwtAuthsInsert, + TIdentityJwtAuthsUpdate + >; [TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType< TIdentityUaClientSecrets, TIdentityUaClientSecretsInsert, @@ -826,5 +906,49 @@ 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 + >; } } 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/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/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/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/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/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/app-connections.ts b/backend/src/db/schemas/app-connections.ts new file mode 100644 index 000000000..8c9dff236 --- /dev/null +++ b/backend/src/db/schemas/app-connections.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 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() +}); + +export type TAppConnections = z.infer; +export type TAppConnectionsInsert = Omit, TImmutableDBKeys>; +export type TAppConnectionsUpdate = Partial, TImmutableDBKeys>>; 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..92fc47c23 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -20,6 +20,7 @@ export * from "./certificates"; export * from "./dynamic-secret-leases"; export * from "./dynamic-secrets"; 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 +31,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 +46,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 +58,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 +67,18 @@ 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-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"; @@ -105,7 +115,13 @@ 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 "./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..b56fab7bf 100644 --- a/backend/src/db/schemas/kms-keys.ts +++ b/backend/src/db/schemas/kms-keys.ts @@ -16,8 +16,7 @@ export const KmsKeysSchema = z.object({ name: z.string(), createdAt: z.date(), updatedAt: z.date(), - projectId: z.string().nullable().optional(), - slug: z.string().nullable().optional() + projectId: z.string().nullable().optional() }); 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 d15e1dff8..c9c1ebda5 100644 --- a/backend/src/db/schemas/kms-root-config.ts +++ b/backend/src/db/schemas/kms-root-config.ts @@ -12,7 +12,7 @@ import { TImmutableDBKeys } from "./models"; export const KmsRootConfigSchema = z.object({ id: z.string().uuid(), encryptedRootKey: zodBuffer, - encryptionStrategy: z.string(), + encryptionStrategy: z.string().default("SOFTWARE").nullable().optional(), createdAt: z.date(), updatedAt: z.date() }); 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..e626443c3 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -2,6 +2,11 @@ import { z } from "zod"; export enum TableName { Users = "users", + 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", @@ -68,12 +73,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 +112,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 +129,18 @@ 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" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; @@ -195,5 +214,22 @@ 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" } diff --git a/backend/src/db/schemas/oidc-configs.ts b/backend/src/db/schemas/oidc-configs.ts index e8030267d..76923aee8 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,22 @@ 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(), + manageGroupMemberships: z.boolean().default(false), + encryptedOidcClientId: zodBuffer, + encryptedOidcClientSecret: zodBuffer }); 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..6639528a0 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -21,7 +21,9 @@ 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() }); 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/projects.ts b/backend/src/db/schemas/projects.ts index deba51b9a..8c1a0386c 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,10 @@ 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) }); 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..f496b29db --- /dev/null +++ b/backend/src/db/schemas/resource-metadata.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 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() +}); + +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..33b13b673 100644 --- a/backend/src/db/schemas/secret-folders.ts +++ b/backend/src/db/schemas/secret-folders.ts @@ -15,7 +15,8 @@ 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() }); export type TSecretFolders = z.infer; 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/ssh-certificate-authorities.ts b/backend/src/db/schemas/ssh-certificate-authorities.ts new file mode 100644 index 000000000..81e789288 --- /dev/null +++ b/backend/src/db/schemas/ssh-certificate-authorities.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 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() +}); + +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..6fe5bc261 --- /dev/null +++ b/backend/src/db/schemas/ssh-certificates.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const SshCertificatesSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + sshCaId: z.string().uuid(), + 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() +}); + +export type TSshCertificates = z.infer; +export type TSshCertificatesInsert = Omit, TImmutableDBKeys>; +export type TSshCertificatesUpdate = 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/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..6a6ec3c07 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -109,7 +109,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..b75a2dae0 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 { 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"; diff --git a/backend/src/ee/routes/v1/dynamic-secret-router.ts b/backend/src/ee/routes/v1/dynamic-secret-router.ts index 4b1566c55..b28d4b18d 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-router.ts @@ -1,5 +1,3 @@ -import slugify from "@sindresorhus/slugify"; -import ms from "ms"; import { z } from "zod"; import { DynamicSecretLeasesSchema } from "@app/db/schemas"; @@ -7,7 +5,9 @@ import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/pro import { 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"; @@ -48,15 +48,7 @@ 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) }), 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..67f955ecb 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 { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; import { 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"; @@ -14,15 +15,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { schema: { 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: { @@ -100,14 +93,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { 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(), @@ -166,7 +152,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 +166,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { }) .merge( z.object({ - isPartOfGroup: z.boolean() + isPartOfGroup: z.boolean(), + joinedGroupAt: z.date().nullable() }) ) .array(), 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..d6b1a4c5c 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 { 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, @@ -33,17 +34,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 +68,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 @@ -103,17 +94,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 +140,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 @@ -189,16 +170,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 diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 5e3a0eafe..e793c687d 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,13 @@ 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 { registerTrustedIpRouter } from "./trusted-ip-router"; import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router"; @@ -61,6 +68,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 +77,15 @@ 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" }); + }, + { prefix: "/ssh" } + ); + await server.register( async (ssoRouter) => { await ssoRouter.register(registerSamlRouter); @@ -80,6 +98,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 +115,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..c9899c98e --- /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 { SymmetricEncryption } 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(SymmetricEncryption) + }), + 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(SymmetricEncryption) + }), + 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..2057677cf 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) => { @@ -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..df5c61fe4 100644 --- a/backend/src/ee/routes/v1/oidc-router.ts +++ b/backend/src/ee/routes/v1/oidc-router.ts @@ -9,19 +9,32 @@ 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 { OidcConfigsSchema } from "@app/db/schemas"; import { OIDCConfigurationType } 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 +}); + 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 +43,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 }); @@ -144,7 +157,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 +168,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { discoveryURL: true, isActive: true, orgId: true, - allowedEmailDomains: true + allowedEmailDomains: true, + manageGroupMemberships: true }).extend({ clientId: z.string(), clientSecret: z.string() @@ -209,12 +223,13 @@ 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() }) .partial() .merge(z.object({ orgSlug: z.string() })), response: { - 200: OidcConfigsSchema.pick({ + 200: SanitizedOidcConfigSchema.pick({ id: true, issuer: true, authorizationEndpoint: true, @@ -225,7 +240,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { userinfoEndpoint: true, orgId: true, allowedEmailDomains: true, - isActive: true + isActive: true, + manageGroupMemberships: true }) } }, @@ -274,7 +290,8 @@ 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) }) .superRefine((data, ctx) => { if (data.configurationType === OIDCConfigurationType.CUSTOM) { @@ -325,19 +342,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 +357,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-template-router.ts b/backend/src/ee/routes/v1/project-template-router.ts index 5b115ab4e..cabf65337 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"; @@ -8,22 +7,13 @@ import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-tem import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; import { 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() @@ -188,9 +178,11 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) schema: { 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( @@ -230,9 +222,10 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) 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), diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index aaebd9b6f..13ac5dfcb 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 || {}) @@ -290,19 +298,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 +339,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..98cb9244d 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,21 @@ 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 }) .extend({ op: z.string(), - tags: tagSchema, + tags: SanitizedTagSchema.array().optional(), + secretMetadata: ResourceMetadataSchema.nullish(), secret: z .object({ id: z.string(), @@ -289,7 +299,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-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..283b9b31e 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 { SecretSnapshotsSchema } from "@app/db/schemas"; import { 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,9 @@ 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() }) .array(), folderVersion: z.object({ id: z.string(), name: z.string() }).array(), @@ -56,6 +52,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, id: req.params.secretSnapshotId }); + return { secretSnapshot }; } }); 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..ab80888d7 --- /dev/null +++ b/backend/src/ee/routes/v1/ssh-certificate-authority-router.ts @@ -0,0 +1,279 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema"; +import { SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types"; +import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema"; +import { 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"; +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; + +export const registerSshCaRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + 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(CertKeyAlgorithm) + .default(CertKeyAlgorithm.RSA_2048) + .describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.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: { + 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: { + 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: { + 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: { + 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: { + 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..a3ce95cb4 --- /dev/null +++ b/backend/src/ee/routes/v1/ssh-certificate-router.ts @@ -0,0 +1,164 @@ +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 { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-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: { + 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 + } + } + }); + + return { + serialNumber, + signedKey: signedPublicKey + }; + } + }); + + server.route({ + method: "POST", + url: "/issue", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + 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(CertKeyAlgorithm) + .default(CertKeyAlgorithm.RSA_2048) + .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(CertKeyAlgorithm) + .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 + } + } + }); + + 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..a85e6b0ca --- /dev/null +++ b/backend/src/ee/routes/v1/ssh-certificate-template-router.ts @@ -0,0 +1,258 @@ +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 { 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: { + 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: { + 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 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: { + 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: { + 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/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..c8f0ba16c 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 { 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) => { @@ -28,18 +30,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) @@ -100,19 +94,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({ diff --git a/backend/src/ee/routes/v2/project-role-router.ts b/backend/src/ee/routes/v2/project-role-router.ts index 70511ce87..0bb83b8d4 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 { 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"; @@ -29,22 +30,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({ @@ -90,24 +86,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({ 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..50670cb49 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"; @@ -100,13 +101,14 @@ export const accessApprovalRequestServiceFactory = ({ 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 +132,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[] = []; @@ -210,7 +215,7 @@ export const accessApprovalRequestServiceFactory = ({ ); 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, @@ -242,7 +247,7 @@ export const accessApprovalRequestServiceFactory = ({ requesterEmail: requestedByUser.email, isTemporary, ...(isTemporary && { - expiresIn: ms(ms(temporaryRange || ""), { long: true }) + expiresIn: msFn(ms(temporaryRange || ""), { long: true }) }), secretPath, environment: envSlug, @@ -270,13 +275,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 +315,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 +431,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/audit-log-stream/audit-log-stream-service.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts index 4f080dac3..ab5e0ee5a 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 @@ -93,7 +93,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({ 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..21f785835 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -39,11 +39,13 @@ export const auditLogDALFactory = (db: TDbClient) => { offset = 0, actorId, actorType, + secretPath, eventType, eventMetadata }: Omit & { actorId?: string; actorType?: ActorType; + secretPath?: string; eventType?: EventType[]; eventMetadata?: Record; }, @@ -88,6 +90,10 @@ export const auditLogDALFactory = (db: TDbClient) => { }); } + if (projectId && secretPath) { + void sqlQuery.whereRaw(`"eventMetadata" @> jsonb_build_object('secretPath', ?::text)`, [secretPath]); + } + // Filter by actor type if (actorType) { void sqlQuery.where("actor", actorType); @@ -100,10 +106,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..295923ef5 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,7 @@ export const auditLogServiceFactory = ({ actorId: filter.auditLogActorId, actorType: filter.actorType, eventMetadata: filter.eventMetadata, + secretPath: filter.secretPath, ...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId }) }); @@ -79,10 +80,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..eef11b8a6 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,27 @@ import { TCreateProjectTemplateDTO, TUpdateProjectTemplateDTO } from "@app/ee/services/project-template/project-template-types"; +import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types"; +import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types"; import { SymmetricEncryption } from "@app/lib/crypto/cipher"; 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: { @@ -20,13 +35,21 @@ export type TListProjectAuditLogDTO = { projectId?: string; auditLogActorId?: string; actorType?: ActorType; + secretPath?: 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; @@ -60,6 +83,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 +118,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 +166,18 @@ 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_CA = "create-certificate-authority", GET_CA = "get-certificate-authority", UPDATE_CA = "update-certificate-authority", @@ -193,6 +234,7 @@ 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", UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping", @@ -202,13 +244,53 @@ 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" } interface UserActorMetadata { userId: string; email?: string | null; username: string; + permission?: Record; } interface ServiceActorMetadata { @@ -219,12 +301,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 +330,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 +350,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 +361,8 @@ interface GetSecretsEvent { }; } +type TSecretMetadata = { key: string; value: string }[]; + interface GetSecretEvent { type: EventType.GET_SECRET; metadata: { @@ -269,6 +371,7 @@ interface GetSecretEvent { secretId: string; secretKey: string; secretVersion: number; + secretMetadata?: TSecretMetadata; }; } @@ -280,6 +383,7 @@ interface CreateSecretEvent { secretId: string; secretKey: string; secretVersion: number; + secretMetadata?: TSecretMetadata; }; } @@ -288,7 +392,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 +410,7 @@ interface UpdateSecretEvent { secretId: string; secretKey: string; secretVersion: number; + secretMetadata?: TSecretMetadata; }; } @@ -307,8 +418,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 +474,13 @@ interface AuthorizeIntegrationEvent { }; } +interface UpdateIntegrationAuthEvent { + type: EventType.UPDATE_INTEGRATION_AUTH; + metadata: { + integration: string; + }; +} + interface UnauthorizeIntegrationEvent { type: EventType.UNAUTHORIZE_INTEGRATION; metadata: { @@ -699,9 +823,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 +845,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; @@ -856,6 +980,7 @@ interface AddIdentityOidcAuthEvent { boundIssuer: string; boundAudiences: string; boundClaims: Record; + claimMetadataMapping: Record; boundSubject: string; accessTokenTTL: number; accessTokenMaxTTL: number; @@ -880,6 +1005,7 @@ interface UpdateIdentityOidcAuthEvent { boundIssuer?: string; boundAudiences?: string; boundClaims?: Record; + claimMetadataMapping?: Record; boundSubject?: string; accessTokenTTL?: number; accessTokenMaxTTL?: number; @@ -895,6 +1021,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 +1148,7 @@ interface CreateFolderEvent { folderId: string; folderName: string; folderPath: string; + description?: string; }; } @@ -1132,6 +1320,127 @@ 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: CertKeyAlgorithm; + 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 CreateCa { type: EventType.CREATE_CA; metadata: { @@ -1602,6 +1911,13 @@ interface GetCmeksEvent { }; } +interface GetCmekEvent { + type: EventType.GET_CMEK; + metadata: { + keyId: string; + }; +} + interface CmekEncryptEvent { type: EventType.CMEK_ENCRYPT; metadata: { @@ -1668,6 +1984,311 @@ 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 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; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -1680,6 +2301,7 @@ export type Event = | DeleteSecretBatchEvent | GetWorkspaceKeyEvent | AuthorizeIntegrationEvent + | UpdateIntegrationAuthEvent | UnauthorizeIntegrationEvent | CreateIntegrationEvent | DeleteIntegrationEvent @@ -1733,6 +2355,11 @@ export type Event = | DeleteIdentityOidcAuthEvent | UpdateIdentityOidcAuthEvent | GetIdentityOidcAuthEvent + | LoginIdentityJwtAuthEvent + | AddIdentityJwtAuthEvent + | UpdateIdentityJwtAuthEvent + | GetIdentityJwtAuthEvent + | DeleteIdentityJwtAuthEvent | CreateEnvironmentEvent | GetEnvironmentEvent | UpdateEnvironmentEvent @@ -1757,6 +2384,17 @@ export type Event = | SecretApprovalClosed | SecretApprovalRequest | SecretApprovalReopened + | SignSshKey + | IssueSshCreds + | CreateSshCa + | GetSshCa + | UpdateSshCa + | DeleteSshCa + | GetSshCaCertificateTemplates + | CreateSshCertificateTemplate + | UpdateSshCertificateTemplate + | GetSshCertificateTemplate + | DeleteSshCertificateTemplate | CreateCa | GetCa | UpdateCa @@ -1812,6 +2450,7 @@ export type Event = | CreateCmekEvent | UpdateCmekEvent | DeleteCmekEvent + | GetCmekEvent | GetCmeksEvent | CmekEncryptEvent | CmekDecryptEvent @@ -1822,4 +2461,42 @@ 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 + | CreateSecretRequestEvent + | SecretApprovalRequestReview; 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/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..43f6197bf 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,13 +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 - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) @@ -103,16 +107,17 @@ export const dynamicSecretLeaseServiceFactory = ({ 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,18 +151,24 @@ 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 - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const plan = await licenseService.getPlan(actorOrgId); if (!plan?.dynamicSecret) { throw new BadRequestError({ @@ -179,15 +190,10 @@ export const dynamicSecretLeaseServiceFactory = ({ 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: 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,18 +231,24 @@ 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 - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) throw new NotFoundError({ @@ -250,12 +262,7 @@ export const dynamicSecretLeaseServiceFactory = ({ 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: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() ) as object; const revokeResponse = await selectedProvider @@ -294,13 +301,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 - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) @@ -336,13 +344,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 - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) 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..57c05b6fb 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,51 @@ +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.NODE_ENV === "development") return; // incase you want to remove this check in dev - 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" }); + 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) + ); - if (host === "localhost" || host === "127.0.0.1" || dbHost === host) { - throw new BadRequestError({ message: "Invalid db host" }); + // 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) { + 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..25ea21024 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,22 @@ 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 { 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 +44,8 @@ type TDynamicSecretServiceFactoryDep = { folderDAL: Pick; projectDAL: Pick; permissionService: Pick; + kmsService: Pick; + projectGatewayDAL: Pick; }; export type TDynamicSecretServiceFactory = ReturnType; @@ -54,7 +58,9 @@ export const dynamicSecretServiceFactory = ({ dynamicSecretProviders, permissionService, dynamicSecretQueueService, - projectDAL + projectDAL, + kmsService, + projectGatewayDAL }: TDynamicSecretServiceFactoryDep) => { const create = async ({ path, @@ -73,13 +79,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 - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.CreateRootCredential, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) @@ -104,23 +111,35 @@ 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 { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); 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, + encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob, maxTTL, defaultTTL, folderId: folder.id, - name + name, + projectGatewayId: selectedGatewayId }); return dynamicSecretCfg; }; @@ -144,13 +163,14 @@ export const dynamicSecretServiceFactory = ({ 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.EditRootCredential, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) @@ -178,34 +198,47 @@ export const dynamicSecretServiceFactory = ({ 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, + encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob, maxTTL, defaultTTL, name: newName ?? name, status: null, - statusDetails: null + statusDetails: null, + projectGatewayId: selectedGatewayId }); return updatedDynamicCfg; @@ -227,13 +260,14 @@ export const dynamicSecretServiceFactory = ({ 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.DeleteRootCredential, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) @@ -287,13 +321,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 - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.ReadRootCredential, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) @@ -311,13 +346,13 @@ export const dynamicSecretServiceFactory = ({ if (!dynamicSecretCfg) { throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` }); } + 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: dynamicSecretCfg.encryptedInput }).toString() ) as object; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object; @@ -337,13 +372,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,13 +416,14 @@ 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 }) @@ -428,13 +465,14 @@ export const dynamicSecretServiceFactory = ({ 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.ReadRootCredential, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) @@ -459,13 +497,14 @@ export const dynamicSecretServiceFactory = ({ { 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( @@ -504,13 +543,14 @@ export const dynamicSecretServiceFactory = ({ ...params }: 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) => 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..992c9098e 100644 --- a/backend/src/ee/services/dynamic-secret/providers/ldap.ts +++ b/backend/src/ee/services/dynamic-secret/providers/ldap.ts @@ -52,7 +52,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 +83,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,7 +191,7 @@ 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); @@ -235,7 +235,7 @@ 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); @@ -268,7 +268,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..a1c3c941b --- /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().replace(/\n/g, "").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().replace(/\n/g, "").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..faaace343 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -1,7 +1,9 @@ +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"; @@ -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) => { @@ -162,7 +181,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 +189,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 +208,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 +301,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 +349,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..b3b61694b --- /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) }; + }; + + 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) }; + }; + + 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..27e847896 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -2,7 +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 { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; +import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; @@ -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,7 +61,8 @@ 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" }); @@ -84,16 +87,35 @@ 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" }); - const group = await groupDAL.create({ - name, - slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`), - orgId: actorOrgId, - role: isCustomRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to create a more privileged group", + 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; @@ -139,27 +161,46 @@ 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 = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to update a more privileged group", + 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; }; @@ -222,7 +263,8 @@ export const groupServiceFactory = ({ actorId, actorAuthMethod, actorOrgId, - search + search, + filter }: TListGroupUsersDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); @@ -251,7 +293,8 @@ export const groupServiceFactory = ({ offset, limit, username, - search + search, + filter }); return { users: members, totalCount }; @@ -280,12 +323,28 @@ 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 = validatePermissionBoundary(permission, groupRolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to add user to more privileged group", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const user = await userDAL.findOne({ username }); if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` }); @@ -335,12 +394,28 @@ 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 = validatePermissionBoundary(permission, groupRolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to delete user from more privileged group", + 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 index f91f9a004..ef975a371 100644 --- a/backend/src/ee/services/hsm/hsm-fns.ts +++ b/backend/src/ee/services/hsm/hsm-fns.ts @@ -1,25 +1,23 @@ import * as pkcs11js from "pkcs11js"; -import { getConfig } from "@app/lib/config/env"; +import { TEnvConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { HsmModule } from "./hsm-types"; -export const initializeHsmModule = () => { - const appCfg = getConfig(); - +export const initializeHsmModule = (envConfig: Pick) => { // Create a new instance of PKCS11 module const pkcs11 = new pkcs11js.PKCS11(); let isInitialized = false; const initialize = () => { - if (!appCfg.isHsmConfigured) { + if (!envConfig.isHsmConfigured) { return; } try { // Load the PKCS#11 module - pkcs11.load(appCfg.HSM_LIB_PATH!); + pkcs11.load(envConfig.HSM_LIB_PATH!); // Initialize the module pkcs11.C_Initialize(); @@ -27,7 +25,7 @@ export const initializeHsmModule = () => { logger.info("PKCS#11 module initialized"); } catch (err) { - logger.error("Failed to initialize PKCS#11 module:", err); + logger.error(err, "Failed to initialize PKCS#11 module"); throw err; } }; @@ -39,7 +37,7 @@ export const initializeHsmModule = () => { isInitialized = false; logger.info("PKCS#11 module finalized"); } catch (err) { - logger.error("Failed to finalize PKCS#11 module:", err); + logger.error(err, "Failed to finalize PKCS#11 module"); throw err; } } diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index a1a0773fc..d35d17a24 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -1,12 +1,13 @@ import pkcs11js from "pkcs11js"; -import { getConfig } from "@app/lib/config/env"; +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; @@ -15,9 +16,7 @@ type SyncOrAsync = T | Promise; type SessionCallback = (session: pkcs11js.Handle) => SyncOrAsync; // eslint-disable-next-line no-empty-pattern -export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsmServiceFactoryDep) => { - const appCfg = getConfig(); - +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; @@ -63,11 +62,11 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm throw new Error("No slots available"); } - if (appCfg.HSM_SLOT >= slots.length) { - throw new Error(`HSM slot ${appCfg.HSM_SLOT} not found or not initialized`); + if (envConfig.HSM_SLOT >= slots.length) { + throw new Error(`HSM slot ${envConfig.HSM_SLOT} not found or not initialized`); } - const slotId = slots[appCfg.HSM_SLOT]; + const slotId = slots[envConfig.HSM_SLOT]; const startTime = Date.now(); while (Date.now() - startTime < MAX_TIMEOUT) { @@ -78,7 +77,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm // Login try { - pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, appCfg.HSM_PIN); + pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, envConfig.HSM_PIN); logger.info("HSM: Successfully authenticated"); break; } catch (error) { @@ -86,7 +85,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm 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 ${appCfg.HSM_SLOT}`); + 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) { @@ -133,7 +132,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm }; const $findKey = (sessionHandle: pkcs11js.Handle, type: HsmKeyType) => { - const label = type === HsmKeyType.HMAC ? `${appCfg.HSM_KEY_LABEL}_HMAC` : appCfg.HSM_KEY_LABEL; + 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 = [ @@ -360,7 +359,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm }; const isActive = async () => { - if (!isInitialized || !appCfg.isHsmConfigured) { + if (!isInitialized || !envConfig.isHsmConfigured) { return false; } @@ -372,11 +371,11 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm logger.error(err, "HSM: Error testing PKCS#11 module"); } - return appCfg.isHsmConfigured && isInitialized && pkcs11TestPassed; + return envConfig.isHsmConfigured && isInitialized && pkcs11TestPassed; }; const startService = async () => { - if (!appCfg.isHsmConfigured || !pkcs11 || !isInitialized) return; + if (!envConfig.isHsmConfigured || !pkcs11 || !isInitialized) return; try { await $withSession(async (sessionHandle) => { @@ -395,7 +394,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm { 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: appCfg.HSM_KEY_LABEL! }, + { 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 @@ -410,7 +409,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm keyTemplate ); - logger.info(`HSM: Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); + logger.info(`HSM: Master key created successfully with label: ${envConfig.HSM_KEY_LABEL}`); } // Check if HMAC key exists, create if not @@ -419,7 +418,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm { 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: `${appCfg.HSM_KEY_LABEL!}_HMAC` }, + { 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 @@ -434,7 +433,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm hmacKeyTemplate ); - logger.info(`HSM: HMAC key created successfully with label: ${appCfg.HSM_KEY_LABEL}_HMAC`); + logger.info(`HSM: HMAC key created successfully with label: ${envConfig.HSM_KEY_LABEL}_HMAC`); } // Get slot info to check supported mechanisms 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..cc66411ee 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,11 +1,12 @@ -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 { ActionProjectType, TableName } from "@app/db/schemas"; +import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission"; +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"; @@ -55,28 +56,40 @@ 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( + ProjectPermissionActions.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 = validatePermissionBoundary(permission, targetIdentityPermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to update more privileged 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 +145,41 @@ 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( + ProjectPermissionActions.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 } = 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 = validatePermissionBoundary(permission, targetIdentityPermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to update more privileged 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 +235,33 @@ 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( + ProjectPermissionActions.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 - ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to update more privileged identity", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); return { @@ -251,14 +286,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( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); return { ...identityPrivilege, @@ -282,14 +321,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( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -314,14 +357,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( + ProjectPermissionActions.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..9cda292f6 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,10 +1,12 @@ -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 { ActionProjectType } from "@app/db/schemas"; +import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +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"; @@ -62,28 +64,38 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ 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( + ProjectPermissionActions.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 = validatePermissionBoundary(permission, targetIdentityPermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to update more privileged identity", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -91,6 +103,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 +155,39 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ 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 - ); - 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( + ProjectPermissionActions.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 = validatePermissionBoundary(permission, targetIdentityPermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to update more privileged identity", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -182,6 +208,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 +263,34 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ 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( + ProjectPermissionActions.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 - ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" }); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to edit more privileged identity", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -287,14 +326,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( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -326,14 +369,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( + ProjectPermissionActions.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..66c3a1d46 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-operation-service.ts @@ -0,0 +1,422 @@ +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 { 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, + 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..dc0a8e85d --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -0,0 +1,817 @@ +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 { isValidHostname, isValidIp } from "@app/lib/ip"; +import { ms } from "@app/lib/ms"; +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 (isValidHostname(altName)) { + 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..a259a79b8 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-types.ts @@ -0,0 +1,102 @@ +import { SymmetricEncryption } 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: SymmetricEncryption; +} & 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: SymmetricEncryption; +} & 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..99b0d8d9b 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); }); 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 70c299564..21d378802 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -24,6 +24,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ rbac: false, customRateLimits: false, customAlerts: false, + secretAccessInsights: false, auditLogs: false, auditLogsRetentionDays: 0, auditLogStreams: false, @@ -49,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..29c36c7fe 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -12,10 +12,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 +31,7 @@ import { TFeatureSet, TGetOrgBillInfoDTO, TGetOrgTaxIdDTO, + TOfflineLicense, TOfflineLicenseContents, TOrgInvoiceDTO, TOrgLicensesDTO, @@ -39,10 +43,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 +56,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( @@ -125,6 +134,7 @@ export const licenseServiceFactory = ({ instanceType = InstanceType.EnterpriseOnPremOffline; logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`); isValidLicense = true; + selfHostedLicense = contents.license; return; } } @@ -142,7 +152,10 @@ export const licenseServiceFactory = ({ 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 +174,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 +183,8 @@ export const licenseServiceFactory = ({ JSON.stringify(onPremFeatures) ); return onPremFeatures; + } finally { + logger.info(`getPlan: Process done for [orgId=${orgId}] [projectId=${projectId}]`); } return onPremFeatures; }; @@ -246,8 +261,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 +358,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 +386,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) => { diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 622b0e06b..34e350cf6 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -48,6 +48,7 @@ export type TFeatureSet = { samlSSO: false; hsm: false; oidcSSO: false; + secretAccessInsights: false; scim: false; ldap: false; groups: false; @@ -67,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-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 17c1ddaaf..52c8dd597 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,21 @@ export const oidcConfigServiceFactory = ({ isActive: oidcCfg.isActive, allowedEmailDomains: oidcCfg.allowedEmailDomains, clientId, - clientSecret + clientSecret, + manageGroupMemberships: oidcCfg.manageGroupMemberships }; }; - 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 +241,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 +249,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 +333,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 +445,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 +480,8 @@ export const oidcConfigServiceFactory = ({ tokenEndpoint, userinfoEndpoint, clientId, - clientSecret + clientSecret, + manageGroupMemberships }: TUpdateOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: orgSlug @@ -389,19 +509,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 +535,16 @@ export const oidcConfigServiceFactory = ({ userinfoEndpoint, jwksUri, isActive, - lastUsed: null + lastUsed: null, + manageGroupMemberships }; 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 +568,8 @@ export const oidcConfigServiceFactory = ({ tokenEndpoint, userinfoEndpoint, clientId, - clientSecret + clientSecret, + manageGroupMemberships }: TCreateOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: orgSlug @@ -482,61 +596,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 +612,9 @@ export const oidcConfigServiceFactory = ({ tokenEndpoint, userinfoEndpoint, orgId: org.id, - encryptedClientId, - clientIdIV, - clientIdTag, - encryptedClientSecret, - clientSecretIV, - clientSecretTag + manageGroupMemberships, + encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob, + encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob }); return oidcCfg; @@ -647,7 +708,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 +724,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..a6bd6ad67 100644 --- a/backend/src/ee/services/oidc/oidc-config-types.ts +++ b/backend/src/ee/services/oidc/oidc-config-types.ts @@ -12,6 +12,8 @@ export type TOidcLoginDTO = { lastName?: string; orgId: string; callbackPort?: string; + groups?: string[]; + manageGroupMemberships?: boolean | null; }; export type TGetOidcCfgDTO = @@ -37,6 +39,7 @@ export type TCreateOidcCfgDTO = { clientSecret: string; isActive: boolean; orgSlug: string; + manageGroupMemberships: boolean; } & TGenericPermission; export type TUpdateOidcCfgDTO = Partial<{ @@ -52,5 +55,6 @@ export type TUpdateOidcCfgDTO = Partial<{ clientSecret: string; isActive: boolean; orgSlug: string; + manageGroupMemberships: boolean; }> & TGenericPermission; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index e0da494c6..8a2646227 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,35 @@ 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 OrgPermissionSubjects { Workspace = "workspace", Role = "role", @@ -27,11 +60,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] @@ -47,12 +87,133 @@ export type OrgPermissionSet = | [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); @@ -125,8 +286,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,7 +314,6 @@ 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); @@ -156,6 +334,10 @@ const buildMemberPermission = () => { 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..ca46442e9 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -125,16 +125,415 @@ export const permissionDALFactory = (db: TDbClient) => { } }; - 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("isGhost", "=", false) + .leftJoin(TableName.GroupProjectMembership, (queryBuilder) => { + void queryBuilder.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])); + }) + .leftJoin( + TableName.GroupProjectMembershipRole, + `${TableName.GroupProjectMembershipRole}.projectMembershipId`, + `${TableName.GroupProjectMembership}.id` + ) + .leftJoin( + { groupCustomRoles: TableName.ProjectRoles }, + `${TableName.GroupProjectMembershipRole}.customRoleId`, + `groupCustomRoles.id` + ) + .join(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`) + .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("orgId").withSchema(TableName.Project), + db.ref("type").withSchema(TableName.Project).as("projectType"), + db.ref("id").withSchema(TableName.Project).as("projectId") + ); + + const userPermissions = sqlNestRelationships({ + data: docs, + key: "userId", + parentMapper: ({ + orgId, + username, + orgAuthEnforced, + membershipId, + groupMembershipId, + membershipCreatedAt, + groupMembershipCreatedAt, + groupMembershipUpdatedAt, + 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.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .leftJoin(TableName.GroupProjectMembership, (queryBuilder) => { void queryBuilder .on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])) - .andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`); + // @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, @@ -268,6 +667,7 @@ 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") ); @@ -283,13 +683,15 @@ export const permissionDALFactory = (db: TDbClient) => { membershipCreatedAt, groupMembershipCreatedAt, groupMembershipUpdatedAt, - membershipUpdatedAt + membershipUpdatedAt, + projectType }) => ({ orgId, orgAuthEnforced, userId, projectId, username, + projectType, id: membershipId || groupMembershipId, createdAt: membershipCreatedAt || groupMembershipCreatedAt, updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt @@ -410,6 +812,163 @@ export const permissionDALFactory = (db: TDbClient) => { } }; + const getProjectIdentityPermissions = async (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` + ) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + void queryBuilder + .on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`); + }) + .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"), + db.ref("permissions").withSchema(TableName.ProjectRoles), + 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 permissions = sqlNestRelationships({ + data: docs, + key: "identityId", + parentMapper: ({ + membershipId, + membershipCreatedAt, + membershipUpdatedAt, + orgId, + identityName, + projectType, + identityId + }) => ({ + id: membershipId, + identityId, + username: identityName, + projectId, + 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 @@ -448,6 +1007,7 @@ export const permissionDALFactory = (db: TDbClient) => { 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"), @@ -479,7 +1039,14 @@ export const permissionDALFactory = (db: TDbClient) => { const permission = sqlNestRelationships({ data: docs, key: "membershipId", - parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({ + parentMapper: ({ + membershipId, + membershipCreatedAt, + membershipUpdatedAt, + orgId, + identityName, + projectType + }) => ({ id: membershipId, identityId, username: identityName, @@ -487,6 +1054,7 @@ export const permissionDALFactory = (db: TDbClient) => { createdAt: membershipCreatedAt, updatedAt: membershipUpdatedAt, orgId, + projectType, // just a prefilled value orgAuthEnforced: false }), @@ -555,6 +1123,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..5a6cf3077 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -1,7 +1,109 @@ +/* eslint-disable no-nested-ternary */ +import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability"; +import { z } from "zod"; + import { TOrganizations } from "@app/db/schemas"; -import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; +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; @@ -29,4 +131,18 @@ 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); +}; + +export { escapeHandlebarsMissingDict, isAuthMethodSaml, validateOrgSSO }; 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..01e2edbe7 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, @@ -192,12 +201,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" }); @@ -218,6 +228,12 @@ export const permissionServiceFactory = ({ validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced); + 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 })) || []; const additionalPrivileges = @@ -227,12 +243,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 +278,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 +302,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 +317,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 +359,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,6 +388,12 @@ 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), @@ -376,20 +417,155 @@ export const permissionServiceFactory = ({ 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 +602,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..c622e65a7 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,6 +17,15 @@ 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", @@ -30,6 +43,24 @@ export enum ProjectPermissionDynamicSecretActions { Lease = "lease" } +export enum ProjectPermissionSecretSyncActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + SyncSecrets = "sync-secrets", + ImportSecrets = "import-secrets", + RemoveSecrets = "remove-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 +85,15 @@ export enum ProjectPermissionSub { CertificateAuthorities = "certificate-authorities", Certificates = "certificates", CertificateTemplates = "certificate-templates", + SshCertificateAuthorities = "ssh-certificate-authorities", + SshCertificates = "ssh-certificates", + SshCertificateTemplates = "ssh-certificate-templates", PkiAlerts = "pki-alerts", PkiCollections = "pki-collections", Kms = "kms", - Cmek = "cmek" + Cmek = "cmek", + SecretSyncs = "secret-syncs", + Kmip = "kmip" } export type SecretSubjectFields = { @@ -82,9 +118,13 @@ export type SecretImportSubjectFields = { secretPath: string; }; +export type IdentityManagementSubjectFields = { + identityId: string; +}; + export type ProjectPermissionSet = | [ - ProjectPermissionActions, + ProjectPermissionSecretActions, ProjectPermissionSub.Secrets | (ForcedSubject & SecretSubjectFields) ] | [ @@ -121,12 +161,20 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] - | [ProjectPermissionActions, ProjectPermissionSub.Identity] + | [ + ProjectPermissionActions, + ProjectPermissionSub.Identity | (ForcedSubject & IdentityManagementSubjectFields) + ] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] + | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] + | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] + | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] | [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 +182,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 +217,7 @@ const SecretConditionV1Schema = 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 }) .partial(); @@ -183,17 +234,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 +254,21 @@ 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 GeneralPermissionSchema = [ z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), @@ -262,12 +318,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 +372,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 +420,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 +457,12 @@ 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." + ) + }), ...GeneralPermissionSchema ]); @@ -380,7 +470,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( @@ -417,6 +507,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ "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(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ), + conditions: IdentityManagementConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), ...GeneralPermissionSchema ]); @@ -427,7 +527,6 @@ const buildAdminPermissionRules = () => { // Admins get full access to everything [ - ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretFolders, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretApproval, @@ -448,7 +547,10 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.Certificates, ProjectPermissionSub.CertificateTemplates, ProjectPermissionSub.PkiAlerts, - ProjectPermissionSub.PkiCollections + ProjectPermissionSub.PkiCollections, + ProjectPermissionSub.SshCertificateAuthorities, + ProjectPermissionSub.SshCertificates, + ProjectPermissionSub.SshCertificateTemplates ].forEach((el) => { can( [ @@ -457,10 +559,22 @@ const buildAdminPermissionRules = () => { ProjectPermissionActions.Create, ProjectPermissionActions.Delete ], - el as ProjectPermissionSub + el ); }); + can( + [ + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Delete + ], + ProjectPermissionSub.Secrets + ); + can( [ ProjectPermissionDynamicSecretActions.ReadRootCredential, @@ -486,6 +600,30 @@ const buildAdminPermissionRules = () => { ], 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 + ); + return rules; }; @@ -496,10 +634,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 ); @@ -633,6 +773,11 @@ 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( [ ProjectPermissionCmekActions.Create, @@ -645,6 +790,19 @@ const buildMemberPermissionRules = () => { ProjectPermissionSub.Cmek ); + can( + [ + ProjectPermissionSecretSyncActions.Create, + ProjectPermissionSecretSyncActions.Edit, + ProjectPermissionSecretSyncActions.Delete, + ProjectPermissionSecretSyncActions.Read, + ProjectPermissionSecretSyncActions.SyncSecrets, + ProjectPermissionSecretSyncActions.ImportSecrets, + ProjectPermissionSecretSyncActions.RemoveSecrets + ], + ProjectPermissionSub.SecretSyncs + ); + return rules; }; @@ -653,7 +811,9 @@ 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); @@ -675,6 +835,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 +861,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 +940,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 +992,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..07bc7d4d3 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,11 +1,12 @@ 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 { ActionProjectType, TableName } from "@app/db/schemas"; +import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +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"; @@ -55,28 +56,34 @@ 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 - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); - const { permission: targetUserPermission } = await permissionService.getProjectPermission( - ActorType.USER, - projectMembership.userId, - projectMembership.projectId, + 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(customPermission)); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to update more privileged user", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ slug, @@ -86,6 +93,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 +151,34 @@ 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 - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); - const { permission: targetUserPermission } = await permissionService.getProjectPermission( - ActorType.USER, - projectMembership.userId, - projectMembership.projectId, + 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 = validatePermissionBoundary(permission, targetUserPermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to update more privileged identity", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); if (dto?.slug) { const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ @@ -173,6 +190,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,13 +245,14 @@ 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 - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); @@ -260,13 +282,14 @@ 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 - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); return { @@ -286,13 +309,14 @@ 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 - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.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-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-service.ts b/backend/src/ee/services/scim/scim-service.ts index 0f814d2ac..c9f2837df 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 }, { @@ -576,7 +607,8 @@ export const scimServiceFactory = ({ firstName, email, lastName, - isEmailVerified: serverCfg.trustSamlEmails + isEmailVerified: + org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails }, tx ); @@ -758,6 +790,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..f72977a9b 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,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { `${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`, db.ref("id").withSchema("secVerTag") ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2)) .select({ secVerTagId: "secVerTag.id", @@ -279,6 +280,11 @@ 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") ); const formatedDoc = sqlNestRelationships({ data: doc, @@ -338,9 +344,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..879065ef5 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,7 @@ type TSecretApprovalRequestServiceFactoryDep = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; @@ -137,18 +148,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 +181,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 +231,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,14 +253,15 @@ 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, + secretMetadata: el.secretMetadata as ResourceMetadataDTO, secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", secretComment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() @@ -268,14 +290,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 +327,7 @@ export const secretApprovalRequestServiceFactory = ({ approvalId, actor, status, + comment, actorId, actorAuthMethod, actorOrgId @@ -323,13 +347,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 +385,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 +422,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 +479,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 +515,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 +577,7 @@ export const secretApprovalRequestServiceFactory = ({ ? await fnSecretV2BridgeBulkInsert({ tx, folderId, + orgId: actorOrgId, inputSecrets: secretCreationCommits.map((el) => ({ tagIds: el?.tags.map(({ id }) => id), version: 1, @@ -531,6 +585,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 +595,7 @@ export const secretApprovalRequestServiceFactory = ({ : [], type: SecretType.Shared })), + resourceMetadataDAL, secretDAL: secretV2BridgeDAL, secretVersionDAL: secretVersionV2BridgeDAL, secretTagDAL, @@ -549,6 +605,7 @@ export const secretApprovalRequestServiceFactory = ({ const updatedSecrets = secretUpdationCommits.length ? await fnSecretV2BridgeBulkUpdate({ folderId, + orgId: actorOrgId, tx, inputSecrets: secretUpdationCommits.map((el) => { const encryptedValue = @@ -573,6 +630,7 @@ export const secretApprovalRequestServiceFactory = ({ skipMultilineEncoding: el.skipMultilineEncoding, key: el.key, tags: el?.tags.map(({ id }) => id), + secretMetadata: el.secretMetadata as ResourceMetadataDTO, ...encryptedValue } }; @@ -580,7 +638,8 @@ export const secretApprovalRequestServiceFactory = ({ secretDAL: secretV2BridgeDAL, secretVersionDAL: secretVersionV2BridgeDAL, secretTagDAL, - secretVersionTagDAL: secretVersionTagV2BridgeDAL + secretVersionTagDAL: secretVersionTagV2BridgeDAL, + resourceMetadataDAL }) : []; const deletedSecret = secretDeletionCommits.length @@ -805,6 +864,7 @@ export const secretApprovalRequestServiceFactory = ({ } await secretQueueService.syncSecrets({ projectId, + orgId: actorOrgId, secretPath: folder.path, environmentSlug: folder.environmentSlug, actorId, @@ -813,7 +873,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 +892,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 +916,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 +1013,7 @@ export const secretApprovalRequestServiceFactory = ({ : keyName2BlindIndex[secretName]; // add tags if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds; + return { ...latestSecretVersions[secretId], ...el, @@ -1107,7 +1169,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 +1200,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 +1251,7 @@ export const secretApprovalRequestServiceFactory = ({ ), skipMultilineEncoding: createdSecret.skipMultilineEncoding, key: createdSecret.secretKey, + secretMetadata: createdSecret.secretMetadata, type: SecretType.Shared })) ); @@ -1221,9 +1285,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 +1308,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 +1341,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 +1408,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 +1448,8 @@ export const secretApprovalRequestServiceFactory = ({ reminderRepeatDays, encryptedValue, secretId, - secretVersion + secretVersion, + secretMetadata }) => ({ version, requestId: doc.id, @@ -1363,7 +1462,8 @@ export const secretApprovalRequestServiceFactory = ({ reminderRepeatDays, reminderNote, encryptedComment, - key + key, + secretMetadata: JSON.stringify(secretMetadata) }) ), tx @@ -1401,7 +1501,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..5fae2675d 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"; @@ -56,6 +58,7 @@ type TSecretReplicationServiceFactoryDep = { >; secretVersionTagDAL: Pick; secretVersionV2TagBridgeDAL: Pick; + resourceMetadataDAL: Pick; secretQueueService: Pick; queueService: Pick; secretApprovalPolicyService: Pick; @@ -121,7 +124,8 @@ export const secretReplicationServiceFactory = ({ secretVersionV2TagBridgeDAL, secretVersionV2BridgeDAL, secretV2BridgeDAL, - kmsService + kmsService, + resourceMetadataDAL }: TSecretReplicationServiceFactoryDep) => { const $getReplicatedSecrets = ( botKey: string, @@ -151,8 +155,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 +184,7 @@ export const secretReplicationServiceFactory = ({ secretPath, environmentSlug, projectId, + orgId, actorId, actor, pickOnlyImportIds, @@ -222,6 +229,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 +265,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 +276,7 @@ export const secretReplicationServiceFactory = ({ ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined })); + const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets); const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key); @@ -333,13 +343,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 +413,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 +433,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 +448,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 +456,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 +477,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 : [] } }; @@ -466,6 +499,7 @@ export const secretReplicationServiceFactory = ({ await secretQueueService.syncSecrets({ projectId, + orgId, secretPath: destinationFolder.path, environmentSlug: destinationFolder.environmentSlug, actorId, @@ -751,6 +785,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/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..e86469c51 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,10 +8,9 @@ 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"; @@ -88,32 +87,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..ac8fcc9f2 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"; @@ -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,6 +333,7 @@ export const secretRotationQueueFactory = ({ await secretVersionV2BridgeDAL.insertMany( updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, + actorType: ActorType.PLATFORM, secretId: id })), tx @@ -369,11 +358,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..8d458111a 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 { + ProjectPermissionActions, + ProjectPermissionSecretActions, + 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,13 +61,14 @@ 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(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); return { @@ -81,13 +90,14 @@ 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, ProjectPermissionSub.SecretRotation @@ -100,7 +110,7 @@ export const secretRotationServiceFactory = ({ }); } ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -154,7 +164,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 +176,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,13 +200,14 @@ 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(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); if (shouldUseSecretV2Bridge) { @@ -234,13 +246,14 @@ 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(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation); await secretRotationQueue.removeFromQueue(doc.id, doc.interval); await secretRotationQueue.addToQueue(doc.id, doc.interval); @@ -251,13 +264,14 @@ 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, ProjectPermissionSub.SecretRotation 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..5fbb3f598 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 @@ -364,7 +425,21 @@ export const secretSnapshotServiceFactory = ({ const secrets = await secretV2BridgeDAL.insertMany( rollbackSnaps.flatMap(({ secretVersions, folderId }) => secretVersions.map( - ({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({ + ({ + latestSecretVersion, + version, + updatedAt, + createdAt, + secretId, + envId, + id, + tags, + // exclude the bottom fields from the secret - they are for versioning only. + userActorId, + identityActorId, + actorType, + ...el + }) => ({ ...el, id: secretId, version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion, @@ -395,8 +470,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..d8240f27e 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"; 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..373bbc640 --- /dev/null +++ b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-validators.ts @@ -0,0 +1,14 @@ +// Validates usernames or wildcard (*) +export const isValidUserPattern = (value: string): boolean => { + // Matches valid Linux usernames or a wildcard (*) + const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/; + return userRegex.test(value); +}; + +// Validates hostnames, wildcard domains, or IP addresses +export const isValidHostPattern = (value: string): boolean => { + // Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses + const hostRegex = + /^(?:\*|\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*|[a-z0-9-]+(?:\.[a-z0-9-]+)*|\d{1,3}(\.\d{1,3}){3}|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(?:%[a-zA-Z0-9]+)?)$/; + return hostRegex.test(value); +}; 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/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..f51f8d639 --- /dev/null +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts @@ -0,0 +1,376 @@ +import { execFile } from "child_process"; +import crypto from "crypto"; +import { promises as fs } from "fs"; +import os from "os"; +import path from "path"; +import { promisify } from "util"; + +import { TSshCertificateTemplates } from "@app/db/schemas"; +import { BadRequestError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; + +import { + isValidHostPattern, + isValidUserPattern +} from "../ssh-certificate-template/ssh-certificate-template-validators"; +import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-types"; + +const execFileAsync = promisify(execFile); + +/* 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: CertKeyAlgorithm) => { + 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; + + switch (keyAlgorithm) { + case CertKeyAlgorithm.RSA_2048: + keyType = "rsa"; + keyBits = "2048"; + break; + case CertKeyAlgorithm.RSA_4096: + keyType = "rsa"; + keyBits = "4096"; + break; + case CertKeyAlgorithm.ECDSA_P256: + keyType = "ecdsa"; + keyBits = "256"; + break; + case CertKeyAlgorithm.ECDSA_P384: + keyType = "ecdsa"; + keyBits = "384"; + break; + default: + throw new BadRequestError({ + message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm" + }); + } + + try { + // Generate the SSH key pair + // The "-N ''" sets an empty passphrase + // The keys are created in the temporary directory + await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""]); + + // 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" }); + 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 (/\r|\n|\t|\0/.test(sanitized)) { + throw new BadRequestError({ + message: `Principal '${sanitized}' contains invalid whitespace or control characters.` + }); + } + + // disallow whitespace anywhere + if (/\s/.test(sanitized)) { + throw new BadRequestError({ + message: `Principal '${sanitized}' cannot contain whitespace.` + }); + } + + // restrict allowed characters to letters, digits, dot, underscore, and hyphen + if (!/^[A-Za-z0-9._-]+$/.test(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 = /^[A-Za-z0-9-]+$/; + if (!regex.test(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]); + } 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(() => {}); + } +}; + +/** + * Create an SSH certificate for a user or host. + */ +export const createSshCert = async ({ + template, + caPrivateKey, + clientPublicKey, + keyId, + principals, + requestedTtl, + certType +}: TCreateSshCertDTO) => { + // 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 + const ttl = validateSshCertificateTtl(template, requestedTtl); + + 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" }); + + // 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(() => {}); + } +}; 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..9ff76efbc --- /dev/null +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-schema.ts @@ -0,0 +1,9 @@ +import { SshCertificateAuthoritiesSchema } from "@app/db/schemas"; + +export const sanitizedSshCa = SshCertificateAuthoritiesSchema.pick({ + id: true, + projectId: true, + friendlyName: true, + status: true, + keyAlgorithm: 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..d7ca511e4 --- /dev/null +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts @@ -0,0 +1,523 @@ +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 { 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, + 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 sshCertificateAuthorityDAL.transaction(async (tx) => { + const ca = await sshCertificateAuthorityDAL.create( + { + projectId, + friendlyName, + status: SshCaStatus.ACTIVE, + keyAlgorithm + }, + tx + ); + + const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm); + + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + await sshCertificateAuthoritySecretDAL.create( + { + sshCaId: ca.id, + encryptedPrivateKey: secretManagerEncryptor({ plainText: Buffer.from(privateKey, "utf8") }).cipherTextBlob + }, + tx + ); + + return { ...ca, publicKey }; + }); + + 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..3f202ebf0 --- /dev/null +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-types.ts @@ -0,0 +1,68 @@ +import { TSshCertificateTemplates } from "@app/db/schemas"; +import { TProjectPermission } from "@app/lib/types"; +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; + +export enum SshCaStatus { + ACTIVE = "active", + DISABLED = "disabled" +} + +export enum SshCertType { + USER = "user", + HOST = "host" +} + +export type TCreateSshCaDTO = { + friendlyName: string; + keyAlgorithm: CertKeyAlgorithm; +} & TProjectPermission; + +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: CertKeyAlgorithm; + 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; +}; 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..8ff07db26 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -1,7 +1,16 @@ 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}`) +} as const; + export type TKeyStoreFactory = ReturnType; // all the key prefixes used must be set here to avoid conflict @@ -23,13 +32,17 @@ 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, + 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 }; diff --git a/backend/src/keystore/memory.ts b/backend/src/keystore/memory.ts new file mode 100644 index 000000000..1fe78cf7e --- /dev/null +++ b/backend/src/keystore/memory.ts @@ -0,0 +1,38 @@ +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"; + }, + 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..ec855168e 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1,3 +1,8 @@ +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 const GROUPS = { CREATE: { name: "The name of the group to create.", @@ -19,7 +24,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.", @@ -237,7 +244,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 +260,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 +329,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 +343,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 +435,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 +447,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 +460,9 @@ 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)" }, GET_KEY: { workspaceId: "The ID of the project to get the key from." @@ -420,7 +479,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 +500,17 @@ 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_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 +631,8 @@ 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." }, GET_BY_ID: { folderId: "The ID of the folder to get details." @@ -571,7 +642,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 +652,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 +670,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 +694,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 +723,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 +755,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 +816,8 @@ 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." }, SECRET_DETAILS_LIST: { projectId: "The ID of the project to list secrets/folders from.", @@ -755,6 +841,8 @@ export const AUDIT_LOGS = { projectId: "Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.", 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.", 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.", @@ -1030,6 +1118,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 +1163,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 +1172,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 +1225,84 @@ 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." + }, + 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 CERTIFICATE_AUTHORITIES = { CREATE: { projectSlug: "Slug of the project to create the CA in.", @@ -1430,6 +1604,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)." @@ -1458,3 +1639,149 @@ 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}.` + }; + }, + 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}.` + }; + }, + DELETE: (app: AppConnection) => ({ + connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to be deleted.` + }) +}; + +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." + }, + 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." + } + } +}; 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..147d12ef7 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,9 @@ 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" +} diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 72e0c0d05..4ca9c1b12 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,6 +56,7 @@ 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, // smtp options SMTP_HOST: zpStr(z.string().optional()), @@ -157,8 +159,18 @@ 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()), @@ -169,7 +181,71 @@ const envSchema = z 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) + 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()), + + /* ----------------------------------------------------------------------------- */ + + /* App Connections ----------------------------------------------------------------------------- */ + + // 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( @@ -199,15 +275,16 @@ const envSchema = z 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/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/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..950bfd29f 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,18 @@ 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; } } @@ -133,3 +142,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/fn/string.ts b/backend/src/lib/fn/string.ts index 26e8f27df..1dc2bbfed 100644 --- a/backend/src/lib/fn/string.ts +++ b/backend/src/lib/fn/string.ts @@ -14,3 +14,5 @@ export const prefixWithSlash = (str: string) => { if (str.startsWith("/")) return str; return `/${str}`; }; + +export const startsWithVowel = (str: string) => /^[aeiou]/i.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..1e247dcde 100644 --- a/backend/src/lib/ip/index.ts +++ b/backend/src/lib/ip/index.ts @@ -103,6 +103,16 @@ export const isValidIpOrCidr = (ip: string): boolean => { return false; }; +export const isValidIp = (ip: string) => { + return net.isIPv4(ip) || net.isIPv6(ip); +}; + +export const isValidHostname = (name: string) => { + const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/; + + return hostnameRegex.test(name); +}; + 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/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..ee48dce5a --- /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, prependTableNameToFindFilter(tableName, value as object)] + : [`${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/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..b8b272017 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -43,6 +43,8 @@ export type RequiredKeys = { 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 68cec8f4b..2400af8ee 100644 --- a/backend/src/lib/validator/index.ts +++ b/backend/src/lib/validator/index.ts @@ -1,3 +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-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/main.ts b/backend/src/main.ts index f8adea33f..d5c54991b 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,12 +1,14 @@ +import "./lib/telemetry/instrumentation"; + import dotenv from "dotenv"; -import path from "path"; +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 { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; @@ -16,49 +18,53 @@ 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); + 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 hsmModule = initializeHsmModule(); + 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 }); + const server = await main({ + db, + auditLogDb, + hsmModule: hsmModule.getModule(), + smtp, + logger, + queue, + keyStore, + redis, + envConfig + }); const bootstrap = await bootstrapCheck({ db }); // eslint-disable-next-line @@ -77,9 +83,17 @@ const run = async () => { process.exit(0); }); + 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..94006b5a5 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -1,5 +1,6 @@ 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"; @@ -7,11 +8,20 @@ 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 +43,8 @@ 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" } export enum QueueJobs { @@ -58,7 +69,11 @@ 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" } export type TQueueJobTypes = { @@ -93,7 +108,7 @@ export type TQueueJobTypes = { }; [QueueName.SecretWebhook]: { name: QueueJobs.SecWebhook; - payload: { projectId: string; environment: string; secretPath: string; depth?: number }; + payload: TWebhookPayloads; }; [QueueName.AccessTokenStatusUpdate]: @@ -181,20 +196,68 @@ 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; + }; }; 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, + 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 () => { + const appCfg = getConfig(); + if (appCfg.SHOULD_INIT_PG_QUEUE) { + 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 +272,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.Job[]) => 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, jobsFn) + ) + ); }; const listen = < @@ -223,6 +310,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 +330,18 @@ 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 stopRepeatableJob = async ( name: T, job: TQueueJobTypes[T]["name"], @@ -250,6 +354,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 +370,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 +390,19 @@ 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 + }; }; diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 6484860ea..26f556508 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -10,23 +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"; @@ -34,19 +38,23 @@ 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, hsmModule, 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: appCfg.isHsmConfigured ? 90_000 : 30_000, ignoreTrailingSlash: true, pluginTimeout: 40_000 @@ -55,6 +63,7 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key 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; @@ -79,13 +88,26 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key 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); @@ -99,15 +121,19 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key await server.register(maintenanceMode); - await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule }); + 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/lib/schemas.ts b/backend/src/server/lib/schemas.ts new file mode 100644 index 000000000..0d8eae848 --- /dev/null +++ b/backend/src/server/lib/schemas.ts @@ -0,0 +1,30 @@ +import slugify from "@sindresorhus/slugify"; +import { z } from "zod"; + +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" }) + .regex(/^[a-zA-Z0-9\-_\s]+$/, "Name can only contain alphanumeric characters, dashes, underscores, and spaces"); 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..0fd18bc29 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,7 @@ import { GatewayTimeoutError, InternalServerError, NotFoundError, + OidcAuthError, RateLimitError, ScimRequestError, UnauthorizedError @@ -26,6 +29,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 +37,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) { + 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/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 88bfaaaae..dac88791f 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1,5 +1,6 @@ import { CronJob } from "cron"; import { Knex } from "knex"; +import { monitorEventLoopDelay } from "perf_hooks"; import { z } from "zod"; import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router"; @@ -27,6 +28,10 @@ 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"; @@ -35,6 +40,12 @@ 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"; @@ -75,15 +86,25 @@ 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 { 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"; @@ -121,6 +142,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"; @@ -170,6 +193,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"; @@ -184,6 +208,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"; @@ -201,6 +228,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"; @@ -219,6 +248,9 @@ 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, { @@ -227,7 +259,8 @@ export const registerRoutes = async ( hsmModule, smtp: smtpService, queue: queueService, - keyStore + keyStore, + envConfig }: { auditLogDb?: Knex; db: Knex; @@ -235,6 +268,7 @@ export const registerRoutes = async ( smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory; + envConfig: TEnvConfig; } ) => { const appCfg = getConfig(); @@ -296,12 +330,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); @@ -340,6 +377,12 @@ 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 kmsDAL = kmskeyDALFactory(db); const internalKmsDAL = internalKmsDALFactory(db); const externalKmsDAL = externalKmsDALFactory(db); @@ -348,10 +391,20 @@ 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 permissionService = permissionServiceFactory({ permissionDAL, @@ -360,10 +413,18 @@ 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 + hsmModule, + envConfig }); const kmsService = kmsServiceFactory({ @@ -373,7 +434,8 @@ export const registerRoutes = async ( internalKmsDAL, orgDAL, projectDAL, - hsmService + hsmService, + envConfig }); const externalKmsService = externalKmsServiceFactory({ @@ -391,13 +453,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, @@ -410,14 +473,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, @@ -425,7 +488,8 @@ export const registerRoutes = async ( samlConfigDAL, licenseService, tokenService, - smtpService + smtpService, + kmsService }); const groupService = groupServiceFactory({ userDAL, @@ -437,7 +501,8 @@ export const registerRoutes = async ( projectBotDAL, projectKeyDAL, permissionService, - licenseService + licenseService, + oidcConfigDAL }); const groupProjectService = groupProjectServiceFactory({ groupDAL, @@ -475,7 +540,6 @@ export const registerRoutes = async ( ldapGroupMapDAL, orgDAL, orgMembershipDAL, - orgBotDAL, groupDAL, groupProjectDAL, projectKeyDAL, @@ -487,7 +551,8 @@ export const registerRoutes = async ( permissionService, licenseService, tokenService, - smtpService + smtpService, + kmsService }); const telemetryService = telemetryServiceFactory({ @@ -511,19 +576,30 @@ 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 }); 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, @@ -544,6 +620,7 @@ export const registerRoutes = async ( groupDAL, orgBotDAL, oidcConfigDAL, + loginService, projectBotService }); const signupService = authSignupServiceFactory({ @@ -570,6 +647,11 @@ export const registerRoutes = async ( }); const superAdminService = superAdminServiceFactory({ userDAL, + identityDAL, + userAliasDAL, + identityTokenAuthDAL, + identityAccessTokenDAL, + identityOrgMembershipDAL, authService: loginService, serverCfgDAL: superAdminDAL, kmsRootConfigDAL, @@ -692,6 +774,22 @@ export const registerRoutes = async ( queueService }); + const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({ + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + sshCertificateTemplateDAL, + sshCertificateDAL, + sshCertificateBodyDAL, + kmsService, + permissionService + }); + + const sshCertificateTemplateService = sshCertificateTemplateServiceFactory({ + sshCertificateTemplateDAL, + sshCertificateAuthorityDAL, + permissionService + }); + const certificateAuthorityService = certificateAuthorityServiceFactory({ certificateAuthorityDAL, certificateAuthorityCertDAL, @@ -742,7 +840,8 @@ export const registerRoutes = async ( pkiAlertDAL, pkiCollectionDAL, permissionService, - smtpService + smtpService, + projectDAL }); const pkiCollectionService = pkiCollectionServiceFactory({ @@ -750,7 +849,8 @@ export const registerRoutes = async ( pkiCollectionItemDAL, certificateAuthorityDAL, certificateDAL, - permissionService + permissionService, + projectDAL }); const projectTemplateService = projectTemplateServiceFactory({ @@ -759,10 +859,84 @@ 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, + secretDAL, + secretV2BridgeDAL, + queueService, projectQueue: projectQueueService, + projectBotService, identityProjectDAL, identityOrgMembershipDAL, projectKeyDAL, @@ -778,6 +952,9 @@ export const registerRoutes = async ( certificateDAL, pkiAlertDAL, pkiCollectionDAL, + sshCertificateAuthorityDAL, + sshCertificateDAL, + sshCertificateTemplateDAL, projectUserMembershipRoleDAL, identityProjectMembershipRoleDAL, keyStore, @@ -786,7 +963,8 @@ export const registerRoutes = async ( certificateTemplateDAL, projectSlackConfigDAL, slackIntegrationDAL, - projectTemplateService + projectTemplateService, + groupProjectDAL }); const projectEnvService = projectEnvServiceFactory({ @@ -829,7 +1007,8 @@ export const registerRoutes = async ( permissionService, webhookDAL, projectEnvDAL, - projectDAL + projectDAL, + kmsService }); const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService }); @@ -842,48 +1021,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, @@ -917,7 +1054,8 @@ export const registerRoutes = async ( secretApprovalPolicyService, secretApprovalRequestSecretDAL, kmsService, - snapshotService + snapshotService, + resourceMetadataDAL }); const secretApprovalRequestService = secretApprovalRequestServiceFactory({ @@ -944,7 +1082,8 @@ export const registerRoutes = async ( projectEnvDAL, userDAL, licenseService, - projectSlackConfigDAL + projectSlackConfigDAL, + resourceMetadataDAL }); const secretService = secretServiceFactory({ @@ -965,14 +1104,17 @@ export const registerRoutes = async ( secretApprovalRequestDAL, secretApprovalRequestSecretDAL, secretV2BridgeService, - secretApprovalRequestService + secretApprovalRequestService, + licenseService }); const secretSharingService = secretSharingServiceFactory({ permissionService, secretSharingDAL, orgDAL, - kmsService + kmsService, + smtpService, + userDAL }); const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ @@ -983,7 +1125,10 @@ export const registerRoutes = async ( projectEnvDAL, projectMembershipDAL, projectDAL, - userDAL + userDAL, + accessApprovalRequestDAL, + additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL, + accessApprovalRequestReviewerDAL }); const accessApprovalRequestService = accessApprovalRequestServiceFactory({ @@ -1020,8 +1165,10 @@ export const registerRoutes = async ( kmsService, secretV2BridgeDAL, secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL, - secretVersionV2BridgeDAL + secretVersionV2BridgeDAL, + resourceMetadataDAL }); + const secretRotationQueue = secretRotationQueueFactory({ telemetryService, secretRotationDAL, @@ -1043,7 +1190,8 @@ export const registerRoutes = async ( secretDAL, folderDAL, projectBotService, - secretV2BridgeDAL + secretV2BridgeDAL, + kmsService }); const integrationService = integrationServiceFactory({ @@ -1132,9 +1280,9 @@ export const registerRoutes = async ( identityKubernetesAuthDAL, identityOrgMembershipDAL, identityAccessTokenDAL, - orgBotDAL, permissionService, - licenseService + licenseService, + kmsService }); const identityGcpAuthService = identityGcpAuthServiceFactory({ identityGcpAuthDAL, @@ -1166,15 +1314,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, @@ -1184,8 +1355,11 @@ export const registerRoutes = async ( dynamicSecretProviders, folderDAL, permissionService, - licenseService + licenseService, + kmsService, + projectGatewayDAL }); + const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ projectDAL, permissionService, @@ -1194,12 +1368,14 @@ export const registerRoutes = async ( dynamicSecretLeaseDAL, dynamicSecretProviders, folderDAL, - licenseService + licenseService, + kmsService }); const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, queueService, secretVersionDAL, + secretDAL, secretFolderVersionDAL: folderVersionDAL, snapshotDAL, identityAccessTokenDAL, @@ -1221,13 +1397,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({ @@ -1245,7 +1429,8 @@ export const registerRoutes = async ( const cmekService = cmekServiceFactory({ kmsDAL, kmsService, - permissionService + permissionService, + projectDAL }); const externalMigrationQueue = externalMigrationQueueFactory({ @@ -1261,7 +1446,8 @@ export const registerRoutes = async ( folderDAL, secretDAL: secretV2BridgeDAL, queueService, - secretV2BridgeService + secretV2BridgeService, + resourceMetadataDAL }); const migrationService = externalMigrationServiceFactory({ @@ -1277,6 +1463,40 @@ 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 + }); + await superAdminService.initServerCfg(); // setup the communication with license key server @@ -1331,6 +1551,7 @@ export const registerRoutes = async ( identityAwsAuth: identityAwsAuthService, identityAzureAuth: identityAzureAuthService, identityOidcAuth: identityOidcAuthService, + identityJwtAuth: identityJwtAuthService, accessApprovalPolicy: accessApprovalPolicyService, accessApprovalRequest: accessApprovalRequestService, secretApprovalPolicy: secretApprovalPolicyService, @@ -1344,6 +1565,8 @@ export const registerRoutes = async ( auditLog: auditLogService, auditLogStream: auditLogStreamService, certificate: certificateService, + sshCertificateAuthority: sshCertificateAuthorityService, + sshCertificateTemplate: sshCertificateTemplateService, certificateAuthority: certificateAuthorityService, certificateTemplate: certificateTemplateService, certificateAuthorityCrl: certificateAuthorityCrlService, @@ -1369,7 +1592,13 @@ export const registerRoutes = async ( workflowIntegration: workflowIntegrationService, migration: migrationService, externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService, - projectTemplate: projectTemplateService + projectTemplate: projectTemplateService, + totp: totpService, + appConnection: appConnectionService, + secretSync: secretSyncService, + kmip: kmipService, + kmipOperation: kmipOperationService, + gateway: gatewayService }); const cronJobs: CronJob[] = []; @@ -1381,7 +1610,8 @@ export const registerRoutes = async ( } server.decorate("store", { - user: userDAL + user: userDAL, + kmipClient: kmipClientDAL }); await server.register(injectIdentity, { userDAL, serviceTokenDAL }); @@ -1412,6 +1642,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..67bd26552 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -7,11 +7,12 @@ import { ProjectRolesSchema, ProjectsSchema, SecretApprovalPoliciesSchema, + SecretTagsSchema, UsersSchema } from "@app/db/schemas"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -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 +31,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 +124,17 @@ 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() }); export const ProjectPermissionSchema = z.object({ @@ -189,10 +224,11 @@ 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 }); @@ -206,6 +242,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, @@ -217,3 +255,11 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ kmsCertificateKeyId: true, auditLogsRetentionDays: 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 6ecebb274..6eb1804f1 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -1,6 +1,7 @@ +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"; @@ -72,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({ @@ -83,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); }); }, @@ -103,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({ @@ -119,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); }); }, @@ -134,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", @@ -149,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); }); }, @@ -183,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); }); }, @@ -196,6 +257,27 @@ 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", @@ -215,7 +297,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); }); }, @@ -238,7 +320,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); }); }, @@ -316,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..e23d52004 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts @@ -0,0 +1,319 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { 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; + }>; + updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>; + sanitizedResponseSchema: z.ZodTypeAny; +}) => { + const appName = APP_CONNECTION_NAME_MAP[app]; + + server.route({ + method: "GET", + url: `/`, + config: { + rateLimit: readLimit + }, + schema: { + 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: { + 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: { + 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: { + 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: { + 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 } = req.body; + + const appConnection = (await server.services.appConnection.createAppConnection( + { name, method, app, credentials, description }, + 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 + } + } + }); + + return { appConnection }; + } + }); + + server.route({ + method: "PATCH", + url: "/:connectionId", + config: { + rateLimit: writeLimit + }, + schema: { + 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 } = req.body; + const { connectionId } = req.params; + + const appConnection = (await server.services.appConnection.updateAppConnection( + { name, credentials, connectionId, description }, + 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 + } + } + }); + + return { appConnection }; + } + }); + + server.route({ + method: "DELETE", + url: `/:connectionId`, + config: { + rateLimit: writeLimit + }, + schema: { + 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..cf8181ec4 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -0,0 +1,101 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +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 { + 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 { 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 +]); + +const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ + AwsConnectionListItemSchema, + GitHubConnectionListItemSchema, + GcpConnectionListItemSchema, + AzureKeyVaultConnectionListItemSchema, + AzureAppConfigurationConnectionListItemSchema, + DatabricksConnectionListItemSchema, + HumanitecConnectionListItemSchema +]); + +export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/options", + config: { + rateLimit: readLimit + }, + schema: { + 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: { + 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/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/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..c2b688a43 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -0,0 +1,22 @@ +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 { registerDatabricksConnectionRouter } from "./databricks-connection-router"; +import { registerGcpConnectionRouter } from "./gcp-connection-router"; +import { registerGitHubConnectionRouter } from "./github-connection-router"; +import { registerHumanitecConnectionRouter } from "./humanitec-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 + }; 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..992a16253 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -1,10 +1,10 @@ /* 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 { 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"; diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 99d57e802..f61b1ea1e 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -1,9 +1,9 @@ -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 { 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"; diff --git a/backend/src/server/routes/v1/certificate-template-router.ts b/backend/src/server/routes/v1/certificate-template-router.ts index 681e40e0d..2217445cb 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 { 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"; diff --git a/backend/src/server/routes/v1/cmek-router.ts b/backend/src/server/routes/v1/cmek-router.ts index 18d13e67f..7aecaee37 100644 --- a/backend/src/server/routes/v1/cmek-router.ts +++ b/backend/src/server/routes/v1/cmek-router.ts @@ -1,4 +1,3 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; @@ -8,21 +7,18 @@ import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64"; import { SymmetricEncryption } from "@app/lib/crypto/cipher"; 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"; -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({ @@ -61,7 +57,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - key: KmsKeysSchema + key: CmekSchema }) } }, @@ -114,7 +110,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - key: KmsKeysSchema + key: CmekSchema }) } }, @@ -158,7 +154,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - key: KmsKeysSchema + key: CmekSchema }) } }, @@ -209,7 +205,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 +234,92 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/keys/:keyId", + config: { + rateLimit: readLimit + }, + schema: { + 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: { + 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", diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 8213cf666..901723647 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -1,10 +1,11 @@ import { ForbiddenError, subject } from "@casl/ability"; import { z } from "zod"; -import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas"; +import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionDynamicSecretActions, + ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { DASHBOARD } from "@app/lib/api-docs"; @@ -15,27 +16,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,6 +100,7 @@ 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), includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets) }), response: { @@ -115,21 +109,24 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).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(), + imports: SecretImportsSchema.omit({ importEnv: true }) + .extend({ + importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() }), + environment: z.string() }) .array() .optional(), totalFolderCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(), totalSecretCount: z.number().optional(), + totalImportCount: z.number().optional(), totalCount: z.number() }) } @@ -146,6 +143,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { orderDirection, includeFolders, includeSecrets, + includeImports, includeDynamicSecrets } = req.query; @@ -162,6 +160,7 @@ 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: @@ -171,6 +170,53 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { let totalFolderCount: number | undefined; let totalDynamicSecretCount: number | undefined; let totalSecretCount: number | undefined; + let totalImportCount: 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,13 +264,14 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalCount: totalFolderCount ?? 0 }; - const { permission } = await server.services.permission.getProjectPermission( - req.permission.type, - req.permission.id, + const { permission } = await server.services.permission.getProjectPermission({ + actor: req.permission.type, + actorId: req.permission.id, projectId, - req.permission.authMethod, - req.permission.orgId - ); + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + actionProjectType: ActionProjectType.SecretManager + }); const allowedDynamicSecretEnvironments = // filter envs user has access to environments.filter((environment) => @@ -291,6 +338,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, @@ -346,10 +394,13 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { folders, dynamicSecrets, secrets, + imports, totalFolderCount, totalDynamicSecretCount, + totalImportCount, totalSecretCount, - totalCount: (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + totalCount: + (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + (totalImportCount ?? 0) }; } }); @@ -390,6 +441,7 @@ 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), @@ -407,15 +459,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { dynamicSecrets: SanitizedDynamicSecretSchema.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(), @@ -597,23 +644,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; + 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; await server.services.auditLog.createAuditLog({ projectId, @@ -692,15 +741,10 @@ 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() @@ -744,6 +788,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { const secrets = await server.services.secret.getSecretsRawByFolderMappings( { + filterByAction: ProjectPermissionSecretActions.DescribeSecret, projectId, folderMappings, filters: { @@ -840,4 +885,134 @@ 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), + 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 } = 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 + }); + + 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-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts index 9199c21f1..82387a3cc 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 @@ -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({ @@ -79,44 +80,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 +131,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({ @@ -172,30 +174,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 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..1cf59e682 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -8,8 +8,7 @@ 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({ @@ -76,39 +75,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 +126,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({ @@ -163,32 +168,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 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..d269072d9 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -8,6 +8,7 @@ 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({ @@ -74,40 +75,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 +122,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({ @@ -164,31 +166,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 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..bb09898a3 --- /dev/null +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -0,0 +1,366 @@ +import { z } from "zod"; + +import { IdentityJwtAuthsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { 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: { + 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: { + 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: { + 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: { + 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: { + 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..0105afd76 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -7,17 +7,24 @@ 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) => { @@ -87,47 +94,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 +148,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({ @@ -183,44 +191,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 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..4f1f75f5c 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -11,11 +11,23 @@ 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() }); @@ -87,42 +99,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 +149,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 +165,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, @@ -190,6 +205,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 +218,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 +264,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, diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index 15e6eabef..344da3383 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -7,6 +7,7 @@ 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 { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { SanitizedProjectSchema } from "../sanitizedSchemas"; @@ -118,6 +119,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 }); @@ -166,7 +168,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({ @@ -328,7 +331,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..d6e7259be 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -7,6 +7,7 @@ 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({ @@ -26,36 +27,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 +75,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({ @@ -110,27 +117,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 +159,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({ @@ -244,7 +260,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({ @@ -299,6 +316,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 }); @@ -357,6 +375,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 }); @@ -408,6 +427,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 }); @@ -457,7 +477,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..5a9363f3d 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -7,6 +7,7 @@ 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, @@ -86,49 +87,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 +143,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, @@ -181,46 +184,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 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 dae37ddb3..185738de6 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -5,6 +5,8 @@ import { 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"; @@ -81,6 +83,67 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) } }); + server.route({ + method: "PATCH", + url: "/:integrationAuthId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + 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: "/", @@ -1008,4 +1071,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..f0c28478c 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -9,6 +9,7 @@ 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"; @@ -130,7 +131,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 +141,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({ @@ -206,6 +209,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 }; } }); 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..21723ef58 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -1,4 +1,3 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { @@ -12,10 +11,12 @@ import { } 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 { 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 +30,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { schema: { response: { 200: z.object({ - organizations: OrganizationsSchema.extend({ - orgAuthMethod: z.string() - }).array() + organizations: sanitizedOrganizationSchema + .extend({ + orgAuthMethod: z.string() + }) + .array() }) } }, @@ -110,6 +113,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { querystring: z.object({ projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId), actorType: z.nativeEnum(ActorType).optional(), + secretPath: z + .string() + .optional() + .transform((val) => (!val ? val : removeTrailingSlash(val))) + .describe(AUDIT_LOGS.EXPORT.secretPath), + // eventType is split with , for multiple values, we need to transform it to array eventType: z .string() @@ -242,24 +251,14 @@ 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() }), response: { 200: z.object({ 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/project-env-router.ts b/backend/src/server/routes/v1/project-env-router.ts index c5ded83e4..705016696 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 { 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"; @@ -124,13 +124,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({ @@ -188,14 +182,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) }), diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index 3c3a1ada4..b5a4a8677 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 { @@ -10,6 +9,7 @@ import { } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { 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"; diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index e5e2f636c..b44e93a66 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -2,9 +2,12 @@ import { z } from "zod"; import { IntegrationsSchema, + ProjectEnvironmentsSchema, ProjectMembershipsSchema, ProjectRolesSchema, ProjectSlackConfigsSchema, + ProjectType, + SecretFoldersSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; @@ -135,7 +138,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 +160,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 }; } @@ -296,7 +303,23 @@ 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), + slug: z + .string() + .trim() + .regex( + /^[a-z0-9]+(?:[_-][a-z0-9]+)*$/, + "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." + ) + .max(64, { message: "Slug must be 64 characters or fewer" }) + .optional() + .describe(PROJECTS.UPDATE.slug) }), response: { 200: z.object({ @@ -313,7 +336,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, update: { name: req.body.name, - autoCapitalization: req.body.autoCapitalization + description: req.body.description, + autoCapitalization: req.body.autoCapitalization, + slug: req.body.slug }, actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, @@ -652,4 +677,31 @@ 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; + } + }); }; diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts index 088afec47..5f8b553f5 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v1/secret-folder-router.ts @@ -9,6 +9,8 @@ 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: "/", @@ -47,7 +49,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .default("/") .transform(prefixWithSlash) .transform(removeTrailingSlash) - .describe(FOLDERS.CREATE.directory) + .describe(FOLDERS.CREATE.directory), + description: z.string().optional().nullable().describe(FOLDERS.CREATE.description) }), response: { 200: z.object({ @@ -65,7 +68,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, @@ -76,7 +80,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 } : {}) } } }); @@ -125,7 +130,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .default("/") .transform(prefixWithSlash) .transform(removeTrailingSlash) - .describe(FOLDERS.UPDATE.directory) + .describe(FOLDERS.UPDATE.directory), + description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description) }), response: { 200: z.object({ @@ -196,7 +202,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .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) @@ -342,11 +349,14 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .default("/") .transform(prefixWithSlash) .transform(removeTrailingSlash) - .describe(FOLDERS.LIST.directory) + .describe(FOLDERS.LIST.directory), + recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive) }), response: { 200: z.object({ - folders: SecretFoldersSchema.array() + folders: SecretFoldersSchema.extend({ + relativePath: z.string().optional() + }).array() }) } }, 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/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..c342f3b73 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -0,0 +1,23 @@ +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 { registerDatabricksSyncRouter } from "./databricks-sync-router"; +import { registerGcpSyncRouter } from "./gcp-sync-router"; +import { registerGitHubSyncRouter } from "./github-sync-router"; +import { registerHumanitecSyncRouter } from "./humanitec-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 +}; 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..31826f86f --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts @@ -0,0 +1,408 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { 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: { + 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: { + 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: { + 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: { + 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: { + 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: { + 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: { + 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: { + 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: { + 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..b2d5e5fc7 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts @@ -0,0 +1,109 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { 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 { 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"; + +const SecretSyncSchema = z.discriminatedUnion("destination", [ + AwsParameterStoreSyncSchema, + AwsSecretsManagerSyncSchema, + GitHubSyncSchema, + GcpSyncSchema, + AzureKeyVaultSyncSchema, + AzureAppConfigurationSyncSchema, + DatabricksSyncSchema, + HumanitecSyncSchema +]); + +const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ + AwsParameterStoreSyncListItemSchema, + AwsSecretsManagerSyncListItemSchema, + GitHubSyncListItemSchema, + GcpSyncListItemSchema, + AzureKeyVaultSyncListItemSchema, + AzureAppConfigurationSyncListItemSchema, + DatabricksSyncListItemSchema, + HumanitecSyncListItemSchema +]); + +export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/options", + config: { + rateLimit: readLimit + }, + schema: { + 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: { + 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-tag-router.ts b/backend/src/server/routes/v1/secret-tag-router.ts index 7d696999e..ed9837084 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 { 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"; @@ -111,14 +111,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { 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: { @@ -153,14 +146,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { 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: { 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..a4570389f 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,10 +95,14 @@ 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]; @@ -86,7 +111,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { firstName: profile.displayName, 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, 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..47456e622 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 { @@ -8,6 +7,7 @@ import { ProjectUserMembershipRolesSchema } from "@app/db/schemas"; import { 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,7 +16,7 @@ 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 @@ -30,7 +30,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 +76,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 }; diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts index b2cc3a8e9..18244068c 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 { @@ -9,6 +8,7 @@ import { } from "@app/db/schemas"; import { 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"; 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..326b8a497 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -5,11 +5,14 @@ import { OrgMembershipsSchema, ProjectMembershipsSchema, ProjectsSchema, + ProjectType, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { 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"; @@ -78,6 +81,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 +110,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 }; @@ -281,7 +288,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,7 +331,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }, schema: { body: z.object({ - name: z.string().trim() + name: GenericResourceNameSchema }), response: { 200: z.object({ @@ -358,21 +365,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - organization: OrganizationsSchema + organization: OrganizationsSchema, + 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 - ); - return { organization }; + 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 }; } }); }; 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..0c3ceb01e 100644 --- a/backend/src/server/routes/v2/project-membership-router.ts +++ b/backend/src/server/routes/v2/project-membership-router.ts @@ -27,7 +27,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 +49,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider projects: [ { id: req.params.projectId, - projectRoleSlug: [ProjectMembershipRole.Member] + projectRoleSlug: req.body.roleSlugs || [ProjectMembershipRole.Member] } ] }); diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index c2aa446b4..84d2ee6cd 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,17 @@ 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 { 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 { 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"; @@ -27,14 +31,6 @@ const projectWithEnv = SanitizedProjectSchema.extend({ environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array() }); -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({ @@ -161,24 +157,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 +180,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({ @@ -242,7 +230,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 +264,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 +297,12 @@ 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) }), response: { 200: SanitizedProjectSchema @@ -330,6 +319,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, update: { name: req.body.name, + description: req.body.description, autoCapitalization: req.body.autoCapitalization }, actorId: req.permission.id, @@ -350,7 +340,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, schema: { 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), @@ -391,7 +381,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, schema: { 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 +503,101 @@ 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: { + 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: { + 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 }; + } + }); }; 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..f854baade 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -1,13 +1,7 @@ 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 { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -18,10 +12,12 @@ 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,12 +25,27 @@ 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({ children: z.lazy(() => SecretReferenceNodeTree.array()) }); +const BaseSecretNameSchema = z.string().trim().min(1); + +const SecretNameSchema = BaseSecretNameSchema.refine( + (el) => !el.includes(" "), + "Secret name cannot contain spaces." +).refine((el) => !el.includes(":"), "Secret name cannot contain colon."); + export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", @@ -50,7 +61,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 +77,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() + }) }) } }, @@ -113,7 +116,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 +133,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() }) }) } @@ -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,46 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); } + return { secrets, imports }; } }); + server.route({ + method: "GET", + url: "/raw/id/:secretId", + config: { + rateLimit: secretsLimit + }, + schema: { + 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", @@ -327,28 +412,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 +452,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 +471,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 } } }); @@ -434,7 +509,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 +525,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 +534,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 +565,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 +585,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 } } }); @@ -539,7 +622,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), @@ -558,19 +641,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 +686,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 +705,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 } } }); @@ -647,7 +741,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 +757,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 +781,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 +844,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 +940,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 +1211,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 +1381,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") ]) @@ -1610,7 +1698,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 +1813,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") ]) @@ -1842,7 +1934,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 +1942,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 +1977,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 +1992,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) })) } } @@ -1940,18 +2038,35 @@ 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())) .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 +2079,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 +2097,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 +2117,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, @@ -2047,7 +2192,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 +2201,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") ]) 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..9da622541 --- /dev/null +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -0,0 +1,56 @@ +export enum AppConnection { + GitHub = "github", + AWS = "aws", + Databricks = "databricks", + GCP = "gcp", + AzureKeyVault = "azure-key-vault", + AzureAppConfiguration = "azure-app-configuration", + Humanitec = "humanitec" +} + +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..fe5130ff1 --- /dev/null +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -0,0 +1,160 @@ +import { TAppConnections } from "@app/db/schemas/app-connections"; +import { generateHash } from "@app/lib/crypto/encryption"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service"; +import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types"; +import { + AwsConnectionMethod, + getAwsConnectionListItem, + validateAwsConnectionCredentials +} from "@app/services/app-connection/aws"; +import { + DatabricksConnectionMethod, + getDatabricksConnectionListItem, + validateDatabricksConnectionCredentials +} from "@app/services/app-connection/databricks"; +import { + GcpConnectionMethod, + getGcpConnectionListItem, + validateGcpConnectionCredentials +} from "@app/services/app-connection/gcp"; +import { + getGitHubConnectionListItem, + GitHubConnectionMethod, + validateGitHubConnectionCredentials +} from "@app/services/app-connection/github"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { + AzureAppConfigurationConnectionMethod, + getAzureAppConfigurationConnectionListItem, + validateAzureAppConfigurationConnectionCredentials +} from "./azure-app-configuration"; +import { + AzureKeyVaultConnectionMethod, + getAzureKeyVaultConnectionListItem, + validateAzureKeyVaultConnectionCredentials +} from "./azure-key-vault"; +import { + getHumanitecConnectionListItem, + HumanitecConnectionMethod, + validateHumanitecConnectionCredentials +} from "./humanitec"; + +export const listAppConnectionOptions = () => { + return [ + getAwsConnectionListItem(), + getGitHubConnectionListItem(), + getGcpConnectionListItem(), + getAzureKeyVaultConnectionListItem(), + getAzureAppConfigurationConnectionListItem(), + getDatabricksConnectionListItem(), + getHumanitecConnectionListItem() + ].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 { app } = appConnection; + switch (app) { + case AppConnection.AWS: + return validateAwsConnectionCredentials(appConnection); + case AppConnection.Databricks: + return validateDatabricksConnectionCredentials(appConnection); + case AppConnection.GitHub: + return validateGitHubConnectionCredentials(appConnection); + case AppConnection.GCP: + return validateGcpConnectionCredentials(appConnection); + case AppConnection.AzureKeyVault: + return validateAzureKeyVaultConnectionCredentials(appConnection); + case AppConnection.AzureAppConfiguration: + return validateAzureAppConfigurationConnectionCredentials(appConnection); + case AppConnection.Humanitec: + return validateHumanitecConnectionCredentials(appConnection); + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Unhandled App Connection ${app}`); + } +}; + +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 HumanitecConnectionMethod.API_TOKEN: + return "API Token"; + 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; +}; 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..8a6c65426 --- /dev/null +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -0,0 +1,11 @@ +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" +}; 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..ef3c16cf8 --- /dev/null +++ b/backend/src/services/app-connection/app-connection-schemas.ts @@ -0,0 +1,37 @@ +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 { 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) => + 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) + }); + +export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) => + 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) + }); 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..e2e55bba0 --- /dev/null +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -0,0 +1,380 @@ +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 { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + decryptAppConnection, + encryptAppConnectionCredentials, + getAppConnectionMethodName, + listAppConnectionOptions, + validateAppConnectionCredentials +} from "@app/services/app-connection/app-connection-fns"; +import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps"; +import { + TAppConnection, + TAppConnectionConfig, + TCreateAppConnectionDTO, + TUpdateAppConnectionDTO, + TValidateAppConnectionCredentials +} from "@app/services/app-connection/app-connection-types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { TAppConnectionDALFactory } from "./app-connection-dal"; +import { ValidateAwsConnectionCredentialsSchema } from "./aws"; +import { awsConnectionService } from "./aws/aws-connection-service"; +import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; +import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault"; +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"; + +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 +}; + +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); + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: validatedCredentials, + orgId: actor.orgId, + kmsService + }); + + try { + const connection = await appConnectionDAL.create({ + orgId: actor.orgId, + encryptedCredentials, + method, + app, + ...params + }); + + 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 + ); + + let encryptedCredentials: undefined | Buffer; + + if (credentials) { + const { app, method } = appConnection as DiscriminativePick; + + 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)}` + }); + + const validatedCredentials = await validateAppConnectionCredentials({ + app, + orgId: actor.orgId, + credentials, + method + } as TAppConnectionConfig); + + if (!validatedCredentials) + throw new BadRequestError({ message: "Unable to validate connection - check credentials" }); + + encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: validatedCredentials, + orgId: actor.orgId, + kmsService + }); + } + + try { + const updatedConnection = await appConnectionDAL.updateById(connectionId, { + orgId: actor.orgId, + encryptedCredentials, + ...params + }); + + 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) + }; +}; 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..7051ecb11 --- /dev/null +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -0,0 +1,93 @@ +import { AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { + TAwsConnection, + TAwsConnectionConfig, + TAwsConnectionInput, + TValidateAwsConnectionCredentials +} from "@app/services/app-connection/aws"; +import { + TDatabricksConnection, + TDatabricksConnectionConfig, + TDatabricksConnectionInput, + TValidateDatabricksConnectionCredentials +} from "@app/services/app-connection/databricks"; +import { + TGitHubConnection, + TGitHubConnectionConfig, + TGitHubConnectionInput, + TValidateGitHubConnectionCredentials +} from "@app/services/app-connection/github"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { + TAzureAppConfigurationConnection, + TAzureAppConfigurationConnectionConfig, + TAzureAppConfigurationConnectionInput, + TValidateAzureAppConfigurationConnectionCredentials +} from "./azure-app-configuration"; +import { + TAzureKeyVaultConnection, + TAzureKeyVaultConnectionConfig, + TAzureKeyVaultConnectionInput, + TValidateAzureKeyVaultConnectionCredentials +} from "./azure-key-vault"; +import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp"; +import { + THumanitecConnection, + THumanitecConnectionConfig, + THumanitecConnectionInput, + TValidateHumanitecConnectionCredentials +} from "./humanitec"; + +export type TAppConnection = { id: string } & ( + | TAwsConnection + | TGitHubConnection + | TGcpConnection + | TAzureKeyVaultConnection + | TAzureAppConfigurationConnection + | TDatabricksConnection + | THumanitecConnection +); + +export type TAppConnectionInput = { id: string } & ( + | TAwsConnectionInput + | TGitHubConnectionInput + | TGcpConnectionInput + | TAzureKeyVaultConnectionInput + | TAzureAppConfigurationConnectionInput + | TDatabricksConnectionInput + | THumanitecConnectionInput +); + +export type TCreateAppConnectionDTO = Pick< + TAppConnectionInput, + "credentials" | "method" | "name" | "app" | "description" +>; + +export type TUpdateAppConnectionDTO = Partial> & { + connectionId: string; +}; + +export type TAppConnectionConfig = + | TAwsConnectionConfig + | TGitHubConnectionConfig + | TGcpConnectionConfig + | TAzureKeyVaultConnectionConfig + | TAzureAppConfigurationConnectionConfig + | TDatabricksConnectionConfig + | THumanitecConnectionConfig; + +export type TValidateAppConnectionCredentials = + | TValidateAwsConnectionCredentials + | TValidateGitHubConnectionCredentials + | TValidateGcpConnectionCredentials + | TValidateAzureKeyVaultConnectionCredentials + | TValidateAzureAppConfigurationConnectionCredentials + | TValidateDatabricksConnectionCredentials + | TValidateHumanitecConnectionCredentials; + +export type TListAwsConnectionKmsKeys = { + connectionId: string; + region: AWSRegion; + destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager; +}; 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..00a44745a --- /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..c06c6f0ed --- /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..a0b74c3d0 --- /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 TValidateAwsConnectionCredentials = 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..9ccfc72b6 --- /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..db59a1558 --- /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 TValidateAzureAppConfigurationConnectionCredentials = + 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..12b1b3f3b --- /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..d784ca100 --- /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 TValidateAzureKeyVaultConnectionCredentials = 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/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..a12fe290c --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts @@ -0,0 +1,92 @@ +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +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) => { + 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..af1a75127 --- /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") +}); + +export 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..e45610b16 --- /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 TValidateDatabricksConnectionCredentials = 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..8f54735e4 --- /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 { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { AppConnection } from "../app-connection-enums"; +import { getAppConnectionMethodName } from "../app-connection-fns"; +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..3c313f205 --- /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..097bed1ef --- /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 TValidateGcpConnectionCredentials = 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..391ba5f96 --- /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..714c87174 --- /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 TValidateGitHubConnectionCredentials = 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..a3f31ed66 --- /dev/null +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-enums.ts @@ -0,0 +1,3 @@ +export enum HumanitecConnectionMethod { + API_TOKEN = "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..0eeb9bfbf --- /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.API_TOKEN] + }; +}; + +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..145f78b85 --- /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.API_TOKEN), + credentials: HumanitecConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method", [ + BaseHumanitecConnectionSchema.extend({ + method: z.literal(HumanitecConnectionMethod.API_TOKEN), + credentials: HumanitecConnectionAccessTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateHumanitecConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(HumanitecConnectionMethod.API_TOKEN) + .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..94613bfba --- /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 TValidateHumanitecConnectionCredentials = 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/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..8dfe69643 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -1,5 +1,6 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; +import { Knex } from "knex"; import { TUsers, UserDeviceSchema } from "@app/db/schemas"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; @@ -17,6 +18,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 +28,14 @@ import { TOauthTokenExchangeDTO, TVerifyMfaTokenDTO } from "./auth-login-type"; -import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type"; +import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "./auth-type"; type TAuthLoginServiceFactoryDep = { userDAL: TUserDALFactory; orgDAL: TOrgDALFactory; tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; + totpService: Pick; }; export type TAuthLoginFactory = ReturnType; @@ -40,20 +43,21 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService, - orgDAL + orgDAL, + totpService }: 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 +98,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 +138,8 @@ export const authLoginServiceFactory = ({ tokenVersionId: tokenSession.id, accessVersion: tokenSession.accessVersion, organizationId, - isMfaVerified + isMfaVerified, + mfaMethod }, cfg.AUTH_SECRET, { expiresIn: cfg.JWT_AUTH_LIFETIME } @@ -140,7 +153,8 @@ export const authLoginServiceFactory = ({ tokenVersionId: tokenSession.id, refreshVersion: tokenSession.refreshVersion, organizationId, - isMfaVerified + isMfaVerified, + mfaMethod }, cfg.AUTH_SECRET, { expiresIn: cfg.JWT_REFRESH_LIFETIME } @@ -353,8 +367,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 +387,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,7 +403,8 @@ export const authLoginServiceFactory = ({ userAgent, ip: ipAddress, organizationId, - isMfaVerified: decodedToken.isMfaVerified + isMfaVerified: decodedToken.isMfaVerified, + mfaMethod: decodedToken.mfaMethod }); return { @@ -458,17 +479,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 +556,8 @@ export const authLoginServiceFactory = ({ userAgent, organizationId: orgId, authMethod: decodedToken.authMethod, - isMfaVerified: true + isMfaVerified: true, + mfaMethod }); return { token, user: userEnc }; 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..3dfaa6532 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -2,14 +2,14 @@ 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 { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; 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"; @@ -77,7 +77,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 +126,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 +174,7 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.create( { - projectId: project.id, + projectId, type, organization, ou, @@ -185,7 +198,7 @@ export const certificateAuthorityServiceFactory = ({ ); const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ - projectId: project.id, + projectId, projectDAL, kmsService }); @@ -292,13 +305,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 +337,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 +363,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 +389,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 +451,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 +722,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 +758,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 +839,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 +986,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, @@ -1127,13 +1149,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); @@ -1455,13 +1478,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, @@ -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..1840652ca 100644 --- a/backend/src/services/certificate-authority/certificate-authority-validators.ts +++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { isValidIp } from "@app/lib/ip"; + const isValidDate = (dateString: string) => { const date = new Date(dateString); return !Number.isNaN(date.getTime()); @@ -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 hostnameRegex.test(name) || 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..0a19ac92f 100644 --- a/backend/src/services/certificate-template/certificate-template-fns.ts +++ b/backend/src/services/certificate-template/certificate-template-fns.ts @@ -1,7 +1,6 @@ -import ms from "ms"; - import { TCertificateTemplates } from "@app/db/schemas"; import { BadRequestError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; export const validateCertificateDetailsAgainstTemplate = ( cert: { diff --git a/backend/src/services/certificate-template/certificate-template-service.ts b/backend/src/services/certificate-template/certificate-template-service.ts index ead3e85cb..9a5cd9269 100644 --- a/backend/src/services/certificate-template/certificate-template-service.ts +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -2,7 +2,7 @@ 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"; @@ -67,13 +67,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 +129,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 +187,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 +214,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, @@ -252,13 +256,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, @@ -336,13 +341,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, @@ -417,13 +423,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, 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..5e74a5bac 100644 --- a/backend/src/services/cmek/cmek-service.ts +++ b/backend/src/services/cmek/cmek-service.ts @@ -1,8 +1,10 @@ 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 { DatabaseErrorCode } from "@app/lib/error-codes"; +import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { TCmekDecryptDTO, @@ -14,111 +16,204 @@ import { import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +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); @@ -132,19 +227,20 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC 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) 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.Decrypt, ProjectPermissionSub.Cmek); @@ -161,6 +257,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC deleteCmekById, listCmeksByProjectId, cmekEncrypt, - cmekDecrypt + cmekDecrypt, + findCmekById, + findCmekByName }; }; 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..986d88fb9 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -1,14 +1,15 @@ 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 { 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 { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } 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 +63,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 } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.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 +102,13 @@ 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 = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to assign group to a more privileged role", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); } // validate custom roles input @@ -127,7 +138,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 +173,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,13 +248,14 @@ export const groupProjectServiceFactory = ({ if (!project) 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 - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups); const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); @@ -257,12 +269,13 @@ 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 = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to assign group to a more privileged role", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); } // validate custom roles input @@ -339,13 +352,14 @@ 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 - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups); const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => { @@ -383,13 +397,14 @@ 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 - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); const groupMemberships = await groupProjectDAL.findByProjectId(project.id); @@ -410,13 +425,14 @@ 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 - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, { 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..5be3dfe4c 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 @@ -7,7 +7,7 @@ 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -16,6 +16,7 @@ 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 +30,7 @@ import { } from "./identity-aws-auth-types"; type TIdentityAwsAuthServiceFactoryDep = { - identityAccessTokenDAL: Pick; + identityAccessTokenDAL: Pick; identityAwsAuthDAL: Pick; identityOrgMembershipDAL: Pick; licenseService: Pick; @@ -126,12 +127,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 +150,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}` }); @@ -339,13 +343,18 @@ export const identityAwsAuthServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to revoke aws auth of identity with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to revoke aws auth of identity with more privileged role", + 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..2cc736f1e 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,7 @@ import { z } from "zod"; const twelveDigitRegex = /^\d{12}$/; -const arnRegex = /^arn:aws:iam::\d{12}:(user\/[\w-]+|role\/[\w-]+|\*)$/; +const arnRegex = /^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/; export const validateAccountIds = z .string() 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..87e076e93 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 @@ -5,7 +5,7 @@ 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -14,6 +14,7 @@ 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 +31,7 @@ type TIdentityAzureAuthServiceFactoryDep = { "findOne" | "transaction" | "create" | "updateById" | "delete" >; identityOrgMembershipDAL: Pick; - identityAccessTokenDAL: Pick; + identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; }; @@ -70,7 +71,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 +100,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 +123,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}` }); @@ -310,13 +316,18 @@ export const identityAzureAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to revoke azure auth of identity with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to revoke azure auth of identity with more privileged role", + 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..321f633ff 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 @@ -5,7 +5,7 @@ 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -14,6 +14,7 @@ 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 +29,7 @@ import { type TIdentityGcpAuthServiceFactoryDep = { identityGcpAuthDAL: Pick; identityOrgMembershipDAL: Pick; - identityAccessTokenDAL: Pick; + identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; }; @@ -138,12 +139,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 +163,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}` }); @@ -358,13 +362,18 @@ export const identityGcpAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to revoke gcp auth of identity with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to revoke gcp auth of identity with more privileged role", + 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..988785d7a --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -0,0 +1,550 @@ +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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { validatePermissionBoundary } from "@app/lib/casl/boundary"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, 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(OrgPermissionActions.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(OrgPermissionActions.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(OrgPermissionActions.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 } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to revoke jwt auth of identity with more privileged role", + 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..e68e4e309 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,22 @@ 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 { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { getConfig } from "@app/lib/config/env"; -import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError, 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 +35,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 +48,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 +69,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 +96,8 @@ export const identityKubernetesAuthServiceFactory = ({ apiVersion: "authentication.k8s.io/v1", kind: "TokenReview", spec: { - token: serviceAccountJwt + token: serviceAccountJwt, + ...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {}) } }, { @@ -128,7 +105,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 +206,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 +232,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}` }); @@ -296,79 +277,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 ); @@ -454,61 +383,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, @@ -544,41 +448,21 @@ export const identityKubernetesAuthServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.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 }; @@ -615,13 +499,17 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to revoke kubernetes auth of identity with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to revoke kubernetes auth of identity with more privileged role", + 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..78d5cd33a 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,25 @@ 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 { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { getConfig } from "@app/lib/config/env"; -import { generateAsymmetricKeyPair } from "@app/lib/crypto"; -import { - decryptSymmetric, - encryptSymmetric, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError, 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 +34,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 +48,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 +65,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 +93,7 @@ export const identityOidcAuthServiceFactory = ({ const client = new JwksClient({ jwksUri, - requestAgent + requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined }); const { kid } = decodedToken.header; @@ -132,7 +110,6 @@ export const identityOidcAuthServiceFactory = ({ message: `Access denied: ${error.message}` }); } - throw error; } @@ -148,7 +125,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 +136,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 +153,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,15 +188,20 @@ 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 }; @@ -212,6 +214,7 @@ export const identityOidcAuthServiceFactory = ({ boundIssuer, boundAudiences, boundClaims, + claimMetadataMapping, boundSubject, accessTokenTTL, accessTokenMaxTTL, @@ -220,8 +223,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}` }); @@ -264,67 +269,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 +304,7 @@ export const identityOidcAuthServiceFactory = ({ boundIssuer, boundAudiences, boundClaims, + claimMetadataMapping, boundSubject, accessTokenTTL, accessTokenMaxTTL, @@ -406,6 +366,7 @@ export const identityOidcAuthServiceFactory = ({ boundIssuer, boundAudiences, boundClaims, + claimMetadataMapping, boundSubject, accessTokenMaxTTL, accessTokenTTL, @@ -415,38 +376,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, @@ -476,27 +418,14 @@ export const identityOidcAuthServiceFactory = ({ 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 }; }; @@ -531,14 +460,18 @@ export const identityOidcAuthServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) { + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to revoke OIDC auth of identity with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to revoke oidc auth of identity with more privileged role", + 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..ff97ae7b6 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -1,12 +1,12 @@ -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 { 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 { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, NotFoundError } 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"; @@ -54,14 +54,20 @@ export const identityProjectServiceFactory = ({ projectId, roles }: TCreateProjectIdentityDTO) => { - 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, + subject(ProjectPermissionSub.Identity, { + identityId + }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Identity); const existingIdentity = await identityProjectDAL.findOne({ identityId, projectId }); if (existingIdentity) @@ -85,11 +91,13 @@ 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 = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to assign to a more privileged role", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); } // validate custom roles input @@ -154,14 +162,18 @@ export const identityProjectServiceFactory = ({ actorAuthMethod, actorOrgId }: TUpdateProjectIdentityDTO) => { - 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, + subject(ProjectPermissionSub.Identity, { identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); const projectIdentity = await identityProjectDAL.findOne({ identityId, projectId }); if (!projectIdentity) @@ -175,14 +187,23 @@ export const identityProjectServiceFactory = ({ projectId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) { - throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" }); - } + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to change to a more privileged role", + 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 +262,34 @@ 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( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Identity, { identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity); - const { permission: identityRolePermission } = await permissionService.getProjectPermission( - ActorType.IDENTITY, - identityId, - identityProjectMembership.projectId, + + const { permission: identityRolePermission } = await permissionService.getProjectPermission({ + actor: ActorType.IDENTITY, + actorId: identityId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId - ); - if (!isAtLeastAsPrivileged(permission, identityRolePermission)) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to remove more privileged identity", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId }); return deletedIdentity; @@ -275,13 +307,14 @@ 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(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); const identityMemberships = await identityProjectDAL.findByProjectId(projectId, { @@ -305,14 +338,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( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); const [identityMembership] = await identityProjectDAL.findByProjectId(projectId, { identityId }); if (!identityMembership) 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..1d327fff3 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 @@ -5,7 +5,7 @@ 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -14,6 +14,7 @@ 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 +60,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}` }); @@ -126,8 +130,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}` }); @@ -218,8 +225,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}` }); @@ -245,11 +255,13 @@ export const identityTokenAuthServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) { + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to revoke Token Auth of identity with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to revoke token auth of identity with more privileged role", + details: { missingPermissions: permissionBoundary.missingPermissions } }); - } const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => { const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx); @@ -269,8 +281,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}` }); @@ -295,10 +310,12 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasPriviledge) + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to create token for identity with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to create token for identity with more privileged role", + details: { missingPermissions: permissionBoundary.missingPermissions } }); const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); @@ -328,12 +345,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 +363,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}` }); @@ -382,11 +402,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 +415,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" @@ -415,10 +438,12 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasPriviledge) + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to update token for identity with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to update token for identity with more privileged role", + details: { missingPermissions: permissionBoundary.missingPermissions } }); const [token] = await identityAccessTokenDAL.update( @@ -440,18 +465,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 }); 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..37aff1b1f 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -8,7 +8,7 @@ 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip"; @@ -17,6 +17,7 @@ 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 +64,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 +112,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 +138,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 +159,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}` }); @@ -367,9 +379,12 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to revoke universal auth of identity with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to revoke universal auth of identity with more privileged role", + details: { missingPermissions: permissionBoundary.missingPermissions } }); const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => { @@ -414,10 +429,12 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasPriviledge) + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to add identity to project with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to create client secret for a more privileged identity.", + details: { missingPermissions: permissionBoundary.missingPermissions } }); const appCfg = getConfig(); @@ -475,9 +492,12 @@ export const identityUaServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to add identity to project with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to get identity client secret with more privileged role", + details: { missingPermissions: permissionBoundary.missingPermissions } }); const identityUniversalAuth = await identityUaDAL.findOne({ @@ -524,9 +544,12 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to read identity client secret of project with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to read identity client secret of identity with more privileged role", + details: { missingPermissions: permissionBoundary.missingPermissions } }); const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId); @@ -566,10 +589,12 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - - if (!isAtLeastAsPrivileged(permission, rolePermission)) + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: "Failed to revoke identity client secret with more privileged role" + name: "PermissionBoundaryError", + message: "Failed to revoke identity client secret with more privileged role", + details: { missingPermissions: permissionBoundary.missingPermissions } }); const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, { 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..92a6795d0 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, @@ -70,6 +71,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 +87,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,6 +190,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"), @@ -200,7 +212,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 +250,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { uaId, awsId, gcpId, + jwtId, kubernetesId, oidcId, azureId, @@ -271,7 +285,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { kubernetesId, oidcId, azureId, - tokenId + tokenId, + jwtId }) } }), diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index fffcbacc2..bd6c8d30c 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -4,11 +4,12 @@ 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, NotFoundError } 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"; @@ -58,9 +59,13 @@ export const identityServiceFactory = ({ 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 = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to create a more privileged identity", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const plan = await licenseService.getPlan(orgId); @@ -108,8 +113,11 @@ 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}` }); @@ -129,9 +137,13 @@ export const identityServiceFactory = ({ actorAuthMethod, actorOrgId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to update a more privileged identity", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); let customRole: TOrgRoles | undefined; if (role) { @@ -141,9 +153,13 @@ 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 = validatePermissionBoundary(permission, rolePermission); + if (!appliedRolePermissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to create a more privileged identity", + details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions } + }); if (isCustomRole) customRole = customOrgRole; } @@ -197,7 +213,16 @@ export const identityServiceFactory = ({ 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}` }); @@ -216,9 +241,13 @@ export const identityServiceFactory = ({ actorAuthMethod, actorOrgId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission); + if (!permissionBoundary.isValid) + throw new ForbiddenRequestError({ + name: "PermissionBoundaryError", + message: "Failed to delete more privileged identity", + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const deletedIdentity = await identityDAL.deleteById(id); diff --git a/backend/src/services/identity/identity-types.ts b/backend/src/services/identity/identity-types.ts index ceaf3ecfc..0eca6b7ee 100644 --- a/backend/src/services/identity/identity-types.ts +++ b/backend/src/services/identity/identity-types.ts @@ -12,10 +12,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 = { 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 636f634a3..eb17c05bb 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -1,28 +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, @@ -34,10 +47,13 @@ import { TIntegrationAuthBitbucketEnvironmentsDTO, TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthChecklyGroupsDTO, + TIntegrationAuthCircleCIOrganizationDTO, TIntegrationAuthGithubEnvsDTO, TIntegrationAuthGithubOrgsDTO, TIntegrationAuthHerokuPipelinesDTO, TIntegrationAuthNorthflankSecretGroupDTO, + TIntegrationAuthOctopusDeployProjectScopeValuesDTO, + TIntegrationAuthOctopusDeploySpacesDTO, TIntegrationAuthQoveryEnvironmentsDTO, TIntegrationAuthQoveryOrgsDTO, TIntegrationAuthQoveryProjectDTO, @@ -48,8 +64,10 @@ import { TIntegrationAuthVercelBranchesDTO, TNorthflankSecretGroup, TOauthExchangeDTO, + TOctopusDeployVariableSet, TSaveIntegrationAccessTokenDTO, TTeamCityBuildConfig, + TUpdateIntegrationAuthDTO, TVercelBranches } from "./integration-auth-types"; import { getIntegrationOptions, Integrations, IntegrationUrls } from "./integration-list"; @@ -80,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; @@ -95,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; }; @@ -139,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 }); @@ -248,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 = { @@ -363,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, @@ -501,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); @@ -535,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); @@ -565,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); @@ -606,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); @@ -634,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); @@ -708,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); @@ -755,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); @@ -789,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); @@ -847,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); @@ -883,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); @@ -924,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); @@ -964,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); @@ -1004,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); @@ -1043,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); @@ -1083,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); @@ -1151,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); @@ -1225,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); @@ -1275,13 +1465,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); @@ -1323,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); @@ -1391,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); @@ -1422,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, @@ -1430,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 }); @@ -1453,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) => { @@ -1486,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, @@ -1521,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, @@ -1528,6 +1960,7 @@ export const integrationAuthServiceFactory = ({ getIntegrationAuth, oauthExchange, saveIntegrationToken, + updateIntegrationAuth, deleteIntegrationAuthById, deleteIntegrationAuths, getIntegrationAuthTeams, @@ -1551,7 +1984,11 @@ export const integrationAuthServiceFactory = ({ 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 0b92c29f1..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; @@ -123,6 +128,10 @@ export type TGetIntegrationAuthTeamCityBuildConfigDTO = { appId: string; } & Omit; +export type TIntegrationAuthCircleCIOrganizationDTO = { + id: string; +} & Omit; + export type TVercelBranches = { ref: string; lastCommit: string; @@ -184,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?: | { @@ -193,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..4b07245ac 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -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 007fccd52..d6c450751 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 { @@ -75,7 +76,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 +93,7 @@ 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", GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com", GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`, @@ -217,9 +218,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: "", @@ -413,8 +414,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 197c1f940..56e506759 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -32,20 +32,28 @@ 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 +307,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 +327,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 +422,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 +459,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 +505,7 @@ const syncSecretsAzureKeyVault = async ({ id: string; // secret URI value: string; attributes: { - enabled: true; + enabled: boolean; created: number; updated: number; recoveryLevel: string; @@ -509,10 +541,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("/"); } @@ -658,6 +699,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 +932,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 +994,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 +1096,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 +1151,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 +1198,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 +1215,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 +1246,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 +1283,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 +1301,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 +1430,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 +1490,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 +1534,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 +1671,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 +2257,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 +2281,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 +2375,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 +2823,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 +2896,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 +3372,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 +3386,7 @@ const syncSecretsTerraformCloud = async ({ ...obj, [secret.attributes.key]: secret }), - {} as Record + {} as Record ); const secretsToAdd: { [key: string]: string } = {}; @@ -3170,7 +3467,8 @@ const syncSecretsTerraformCloud = async ({ attributes: { key, value: secrets[key]?.value, - category: integration.targetService + category: integration.targetService, + sensitive: true } } }, @@ -3183,7 +3481,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 +3495,8 @@ const syncSecretsTerraformCloud = async ({ id: terraformSecrets[key].id, attributes: { ...terraformSecrets[key], - value: secrets[key]?.value + value: secrets[key]?.value, + sensitive: true } } }, @@ -3495,17 +3798,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; + }); } }; @@ -3813,10 +4127,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" @@ -3832,7 +4146,6 @@ 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_-]*[^/]$"); - for await (const key of Object.keys(secrets)) { if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { if (!(key in res)) { @@ -3840,7 +4153,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, @@ -3857,7 +4170,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, @@ -3878,16 +4191,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" } - ); + }); } } }; @@ -4195,6 +4505,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] * @@ -4227,7 +4592,7 @@ export const syncIntegrationSecrets = async ({ secretPath: string; }; integrationAuth: TIntegrationAuths; - secrets: Record; + secrets: Record; accessId: string | null; awsAssumeRoleArn: string | null; accessToken: string; @@ -4306,7 +4671,8 @@ export const syncIntegrationSecrets = async ({ integration, integrationAuth, secrets, - accessToken + accessToken, + createManySecretsRawFn }); break; case Integrations.NETLIFY: @@ -4331,7 +4697,8 @@ export const syncIntegrationSecrets = async ({ integrationAuth, integration, secrets, - accessToken + accessToken, + createManySecretsRawFn }); break; case Integrations.RENDER: @@ -4507,6 +4874,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-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 007d33e61..cfd64a89a 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -4,14 +4,16 @@ import { z } from "zod"; 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 { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; -import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig } from "@app/lib/config/env"; +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 { generateHash } from "@app/lib/crypto/encryption"; @@ -35,6 +37,8 @@ import { TEncryptWithKmsDataKeyDTO, TEncryptWithKmsDTO, TGenerateKMSDTO, + TGetKeyMaterialDTO, + TImportKeyMaterialDTO, TUpdateProjectSecretManagerKmsKeyDTO } from "./kms-types"; @@ -42,23 +46,22 @@ 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_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, @@ -291,6 +294,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."); } @@ -314,6 +327,72 @@ 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(SymmetricEncryption.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 }: TImportKeyMaterialDTO, + tx?: Knex + ) => { + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + + const expectedByteLength = getByteLengthForAlgorithm(algorithm); + 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, + 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 encryptWithKmsKey = async ({ kmsId }: Omit, tx?: Knex) => { const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx); if (!kmsDoc) { @@ -353,6 +432,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."); } @@ -451,7 +540,8 @@ export const kmsServiceFactory = ({ } const kmsDecryptor = await decryptWithKmsKey({ - kmsId: kmsKeyId + kmsId: kmsKeyId, + tx: trx }); return kmsDecryptor({ @@ -613,10 +703,8 @@ export const kmsServiceFactory = ({ }; const $getBasicEncryptionKey = () => { - const appCfg = getConfig(); - - const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY; - const isBase64 = !appCfg.ENCRYPTION_KEY; + 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?" @@ -852,54 +940,33 @@ export const kmsServiceFactory = ({ return { id, name, orgId, isExternal }; }; - // akhilmhdh: a copy of this is made in migrations/utils/kms const startService = async () => { - 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") + 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; + + 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; }); - } - // check if KMS root key was already generated and saved in DB - const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); - - // case 1: a root key already exists in the DB - if (kmsRootConfig) { - if (lock) await lock.release(); - logger.info(`KMS: Encrypted ROOT Key found from DB. Decrypting. [strategy=${kmsRootConfig.encryptionStrategy}]`); - - const decryptedRootKey = await $decryptRootKey(kmsRootConfig); - - // set the flag so that other instance 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; - return; - } - - // case 2: no config is found, so we create a new root key with basic encryption - 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; }); - 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 - }); + const decryptedRootKey = await $decryptRootKey(kmsRootConfig); - // set the flag so that other instance 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; + logger.info("KMS: Loading ROOT Key into Memory."); + ROOT_ENCRYPTION_KEY = decryptedRootKey; }; const updateEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => { @@ -945,6 +1012,8 @@ export const kmsServiceFactory = ({ getProjectKeyBackup, loadProjectKeyBackup, getKmsById, - createCipherPairWithDataKey + createCipherPairWithDataKey, + getKeyMaterial, + importKeyMaterial }; }; diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index f655d4b5d..8be0b29fc 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -61,3 +61,15 @@ export enum RootKeyEncryptionStrategy { Software = "SOFTWARE", HSM = "HSM" } +export type TGetKeyMaterialDTO = { + kmsId: string; +}; + +export type TImportKeyMaterialDTO = { + key: Buffer; + algorithm: SymmetricEncryption; + name?: string; + isReserved: boolean; + projectId: string; + orgId: string; +}; diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 24f1d55b0..221840e1b 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -14,6 +14,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,13 +23,78 @@ 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 })[]> => { try { @@ -398,6 +465,7 @@ export const orgDALFactory = (db: TDbClient) => { findAllOrgMembers, countAllOrgMembers, findOrgById, + findOrgBySlug, findAllOrgsByUserId, ghostUserExists, findOrgMembersByUsername, diff --git a/backend/src/services/org/org-schema.ts b/backend/src/services/org/org-schema.ts new file mode 100644 index 000000000..8f5b85403 --- /dev/null +++ b/backend/src/services/org/org-schema.ts @@ -0,0 +1,16 @@ +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 +}); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 3d74f80b6..4ef52a551 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,11 +16,14 @@ 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, + OrgPermissionSecretShareAction, + OrgPermissionSubjects +} from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; @@ -32,11 +36,13 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro 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 +54,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,6 +73,7 @@ import { TGetOrgMembershipDTO, TInviteUserToOrgDTO, TListProjectMembershipsByOrgMembershipIdDTO, + TResendOrgMemberInvitationDTO, TUpdateOrgDTO, TUpdateOrgMembershipDTO, TVerifyUserToOrgDTO @@ -70,6 +81,9 @@ import { type TOrgServiceFactoryDep = { userAliasDAL: Pick; + secretDAL: Pick; + secretV2BridgeDAL: Pick; + folderDAL: Pick; orgDAL: TOrgDALFactory; orgBotDAL: TOrgBotDALFactory; orgRoleDAL: TOrgRoleDALFactory; @@ -98,6 +112,8 @@ type TOrgServiceFactoryDep = { projectBotDAL: Pick; projectUserMembershipRoleDAL: Pick; projectBotService: Pick; + queueService: Pick; + loginService: Pick; }; export type TOrgServiceFactory = ReturnType; @@ -105,6 +121,9 @@ export type TOrgServiceFactory = ReturnType; export const orgServiceFactory = ({ userAliasDAL, orgDAL, + secretDAL, + secretV2BridgeDAL, + folderDAL, userDAL, groupDAL, orgRoleDAL, @@ -125,7 +144,9 @@ export const orgServiceFactory = ({ projectBotDAL, projectUserMembershipRoleDAL, identityMetadataDAL, - projectBotService + projectBotService, + queueService, + loginService }: TOrgServiceFactoryDep) => { /* * Get organization details by the organization id @@ -187,26 +208,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.findAllProjects(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) => { @@ -268,13 +290,29 @@ export const orgServiceFactory = ({ actorOrgId, actorAuthMethod, orgId, - data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug, enforceMfa } + data: { + name, + slug, + authEnforced, + scimEnabled, + defaultMembershipRoleSlug, + enforceMfa, + selectedMfaMethod, + allowSecretSharingOutsideOrganization + } }: 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,6 +343,11 @@ 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) { @@ -333,7 +376,9 @@ export const orgServiceFactory = ({ authEnforced, scimEnabled, defaultMembershipRole, - enforceMfa + enforceMfa, + selectedMfaMethod, + allowSecretSharingOutsideOrganization }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); return org; @@ -412,24 +457,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 +605,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 +708,7 @@ export const orgServiceFactory = ({ } }) : []; + if (projectsToInvite.length !== invitedProjects?.length) { throw new ForbiddenRequestError({ message: "Access denied to one or more of the specified projects" @@ -686,13 +856,14 @@ 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 } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(projectPermission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.Member @@ -1131,6 +1302,8 @@ export const orgServiceFactory = ({ createIncidentContact, deleteIncidentContact, getOrgGroups, - listProjectMembershipsByOrgMembershipId + listProjectMembershipsByOrgMembershipId, + findOrgBySlug, + resendOrgMemberInvitation }; }; diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 5b44eeea5..3c6faeae9 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,6 +71,8 @@ export type TUpdateOrgDTO = { scimEnabled: boolean; defaultMembershipRoleSlug: string; enforceMfa: boolean; + selectedMfaMethod: MfaMethod; + allowSecretSharingOutsideOrganization: boolean; }>; } & TOrgPermission; @@ -73,3 +81,8 @@ 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..b56c0a43e 100644 --- a/backend/src/services/project-key/project-key-service.ts +++ b/backend/src/services/project-key/project-key-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 } from "@app/lib/errors"; @@ -31,13 +32,14 @@ export const projectKeyServiceFactory = ({ nonce, encryptedKey }: TUploadProjectKeyDTO) => { - 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.Member); const receiverMembership = await projectMembershipDAL.findOne({ @@ -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,13 +81,14 @@ export const projectKeyServiceFactory = ({ actorAuthMethod, projectId }: TGetLatestProjectKeyDTO) => { - 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.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..48b65e8f8 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -1,16 +1,16 @@ /* 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, 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 { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError } 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,13 +78,14 @@ export const projectMembershipServiceFactory = ({ includeGroupMembers, projectId }: TGetProjectMembershipDTO) => { - 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.Member); const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); @@ -121,13 +122,14 @@ export const projectMembershipServiceFactory = ({ projectId, username }: TGetProjectMembershipByUsernameDTO) => { - 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.Member); const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { username }); @@ -143,13 +145,14 @@ export const projectMembershipServiceFactory = ({ projectId, id }: TGetProjectMembershipByIdDTO) => { - 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.Member); const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { id }); @@ -169,13 +172,14 @@ 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 - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); const orgMembers = await orgDAL.findMembership({ [`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId, @@ -249,13 +253,14 @@ export const projectMembershipServiceFactory = ({ membershipId, roles }: TUpdateProjectMembershipDTO) => { - 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.Member); const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId); @@ -269,18 +274,23 @@ export const projectMembershipServiceFactory = ({ projectId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); - - if (!hasRequiredPriviledges) { + const permissionBoundary = validatePermissionBoundary(permission, rolePermission); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ - message: `Failed to change to a more privileged role ${requestedRoleChange}` + name: "PermissionBoundaryError", + message: `Failed to change to a more privileged role ${requestedRoleChange}`, + 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,13 +353,14 @@ export const projectMembershipServiceFactory = ({ projectId, membershipId }: TDeleteProjectMembershipOldDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); const member = await userDAL.findUserByProjectMembershipId(membershipId); @@ -378,13 +389,14 @@ export const projectMembershipServiceFactory = ({ emails, usernames }: TDeleteProjectMembershipsDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); const project = await projectDAL.findById(projectId); diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 55564bc17..3dfe11d2c 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,7 +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, permissions: data.permissions ? data.permissions : undefined @@ -144,13 +153,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 +195,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 +219,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..896e1b858 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -1,7 +1,14 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProjectsUpdate } from "@app/db/schemas"; +import { + ProjectsSchema, + ProjectType, + ProjectUpgradeStatus, + ProjectVersion, + TableName, + TProjectsUpdate +} from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; @@ -12,12 +19,18 @@ export type TProjectDALFactory = ReturnType; export const projectDALFactory = (db: TDbClient) => { const projectOrm = ormify(db, TableName.Project); - const findAllProjects = async (userId: string) => { + const findAllProjects = 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 +44,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 +124,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 +212,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 +265,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 +289,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,6 +336,22 @@ 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}` }); + } + }; + return { ...projectOrm, findAllProjects, @@ -317,6 +362,7 @@ export const projectDALFactory = (db: TDbClient) => { findProjectByFilter, findProjectBySlug, findProjectWithOrg, - checkProjectUpgradeStatus + checkProjectUpgradeStatus, + getProjectFromSplitId }; }; 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..dbef33c10 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1,25 +1,40 @@ import { ForbiddenError } 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, + 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 { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; +import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal"; import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; 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,13 +44,17 @@ 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 { TUserDALFactory } from "../user/user-dal"; @@ -53,6 +72,9 @@ import { TListProjectCertificateTemplatesDTO, TListProjectCertsDTO, TListProjectsDTO, + TListProjectSshCasDTO, + TListProjectSshCertificatesDTO, + TListProjectSshCertificateTemplatesDTO, TLoadProjectKmsBackupDTO, TToggleProjectAutoCapitalizationDTO, TUpdateAuditLogsRetentionDTO, @@ -75,13 +97,17 @@ type TProjectServiceFactoryDep = { projectDAL: TProjectDALFactory; 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; + groupProjectDAL: Pick; projectSlackConfigDAL: Pick; slackIntegrationDAL: Pick; projectUserMembershipRoleDAL: Pick; @@ -90,13 +116,18 @@ type TProjectServiceFactoryDep = { certificateTemplateDAL: Pick; pkiAlertDAL: Pick; pkiCollectionDAL: Pick; + sshCertificateAuthorityDAL: Pick; + sshCertificateDAL: Pick; + sshCertificateTemplateDAL: Pick; permissionService: TPermissionServiceFactory; orgService: Pick; licenseService: Pick; + queueService: Pick; + orgDAL: Pick; keyStore: Pick; projectBotDAL: Pick; - projectRoleDAL: Pick; + projectRoleDAL: Pick; kmsService: Pick< TKmsServiceFactory, | "updateProjectSecretManagerKmsKey" @@ -113,9 +144,13 @@ export type TProjectServiceFactory = ReturnType; export const projectServiceFactory = ({ projectDAL, + secretDAL, + secretV2BridgeDAL, projectQueue, projectKeyDAL, permissionService, + queueService, + projectBotService, orgDAL, userDAL, folderDAL, @@ -133,12 +168,16 @@ export const projectServiceFactory = ({ certificateTemplateDAL, pkiCollectionDAL, pkiAlertDAL, + sshCertificateAuthorityDAL, + sshCertificateDAL, + sshCertificateTemplateDAL, keyStore, kmsService, projectBotDAL, projectSlackConfigDAL, slackIntegrationDAL, - projectTemplateService + projectTemplateService, + groupProjectDAL }: TProjectServiceFactoryDep) => { /* * Create workspace. Make user the admin @@ -149,14 +188,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 +246,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, @@ -368,20 +410,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 +421,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 +441,57 @@ 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); 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 +499,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.findAllProjects(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 +544,50 @@ 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, + slug: update.slug }); + return updatedProject; }; @@ -509,16 +599,21 @@ 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; }; @@ -537,13 +632,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 +664,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 +697,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 +719,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 +757,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 +794,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 +819,7 @@ export const projectServiceFactory = ({ const cas = await certificateAuthorityDAL.find( { - projectId: project.id, + projectId, ...(status && { status }), ...(friendlyName && { friendlyName }), ...(commonName && { commonName }) @@ -736,18 +845,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 +879,7 @@ export const projectServiceFactory = ({ ); const count = await certificateDAL.countCertificatesInProject({ - projectId: project.id, + projectId, friendlyName, commonName }); @@ -776,19 +894,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 +931,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 +967,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 +1003,118 @@ 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 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 +1123,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 +1151,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 +1181,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 +1204,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 +1237,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 +1286,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); @@ -1089,6 +1354,9 @@ export const projectServiceFactory = ({ listProjectAlerts, listProjectPkiCollections, listProjectCertificateTemplates, + listProjectSshCas, + listProjectSshCertificates, + listProjectSshCertificateTemplates, updateVersionLimit, updateAuditLogsRetention, updateProjectKmsKey, diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 28cda2d95..5ccf33d23 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -1,11 +1,20 @@ import { Knex } from "knex"; -import { TProjectKeys } from "@app/db/schemas"; +import { ProjectType, TProjectKeys } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; 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 +38,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 = { @@ -69,7 +80,9 @@ export type TUpdateProjectDTO = { filter: Filter; update: { name?: string; + description?: string; autoCapitalization?: boolean; + slug?: string; }; } & Omit; @@ -82,6 +95,7 @@ export type TDeleteProjectDTO = { export type TListProjectsDTO = { includeRoles: boolean; + type?: ProjectType | "all"; } & Omit; export type TUpgradeProjectDTO = { @@ -128,6 +142,13 @@ export type TGetProjectKmsKey = TProjectPermission; export type TListProjectCertificateTemplatesDTO = TProjectPermission; +export type TListProjectSshCasDTO = TProjectPermission; +export type TListProjectSshCertificateTemplatesDTO = TProjectPermission; +export type TListProjectSshCertificatesDTO = { + offset: number; + limit: number; +} & TProjectPermission; + export type TGetProjectSlackConfig = TProjectPermission; export type TUpdateProjectSlackConfig = { diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index dab70806f..f0d579cf7 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -5,6 +5,7 @@ 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"; @@ -16,9 +17,10 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = { identityUniversalAuthClientSecretDAL: Pick; secretVersionDAL: Pick; secretVersionV2DAL: Pick; + secretDAL: Pick; secretFolderVersionDAL: Pick; snapshotDAL: Pick; - secretSharingDAL: Pick; + secretSharingDAL: Pick; queueService: TQueueServiceFactory; }; @@ -30,6 +32,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ snapshotDAL, secretVersionDAL, secretFolderVersionDAL, + secretDAL, identityAccessTokenDAL, secretSharingDAL, secretVersionV2DAL, @@ -37,10 +40,12 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ }: 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(); 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 7b25ad4ed..c03d4ffc0 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -1,7 +1,7 @@ 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"; @@ -41,12 +41,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", @@ -69,9 +69,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] ) ) @@ -107,7 +105,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) @@ -117,6 +114,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) => @@ -128,21 +130,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; @@ -152,13 +153,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[]) => @@ -220,19 +215,12 @@ export const secretFolderDALFactory = (db: TDbClient) => { 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 } }; @@ -250,22 +238,13 @@ export const secretFolderDALFactory = (db: TDbClient) => { }); try { - const pathDepth = removeTrailingSlash(path).split("/").filter(Boolean).length + 1; - + 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; @@ -323,7 +302,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), @@ -493,6 +471,7 @@ export const secretFolderDALFactory = (db: TDbClient) => { 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..d4f58a1d4 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,19 @@ export const secretFolderServiceFactory = ({ orderBy, orderDirection, limit, - offset + offset, + recursive }: 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 +421,17 @@ 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) => folder.id !== parentFolder.id) + .map((folder) => ({ + ...folder, + relativePath: folder.path + })); + } + const folders = await folderDAL.find( { envId: env.id, @@ -432,7 +461,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 +503,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 +539,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 +568,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 +593,44 @@ 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) => ({ + ...folder, + path: buildFolderPath(folder, foldersMap) + })); + + return [env.slug, { ...env, folders: foldersWithPath }]; + }) + ); + + return environmentFolders; + }; + return { createFolder, updateFolder, @@ -545,6 +640,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..2a5681b5b 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,7 @@ export type TGetFolderDTO = { orderDirection?: OrderByDirection; limit?: number; offset?: number; + recursive?: boolean; } & TProjectPermission; export type TGetFolderByIdDTO = { diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index d75a25514..e5a450441 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[] = []; @@ -227,7 +244,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 +284,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..fdd326b5f 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"; @@ -73,13 +81,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 +97,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 +166,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 +176,7 @@ export const secretImportServiceFactory = ({ } else { await secretQueueService.syncSecrets({ secretPath, + orgId: actorOrgId, projectId, environmentSlug: environment, actorId, @@ -189,13 +198,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 }) @@ -283,13 +294,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,6 +348,7 @@ export const secretImportServiceFactory = ({ await secretQueueService.syncSecrets({ secretPath, + orgId: actorOrgId, projectId, environmentSlug: environment, actor, @@ -354,13 +368,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 +407,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 +422,7 @@ export const secretImportServiceFactory = ({ if (membership && sourceFolder) { await secretQueueService.replicateSecrets({ + orgId: actorOrgId, secretPath: secretImportDoc.importPath, projectId, environmentSlug: secretImportDoc.importEnv.slug, @@ -432,13 +445,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 +469,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 +533,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 +583,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 +632,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 +650,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 +668,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 +695,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 +718,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 +732,67 @@ 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(); + }; + return { createImport, updateImport, @@ -683,6 +803,8 @@ export const secretImportServiceFactory = ({ getRawSecretsFromImports, resyncSecretImportReplication, getProjectImportCount, - fnSecretsFromImports + fnSecretsFromImports, + getProjectImportMultiEnvCount, + getImportsMultiEnv }; }; 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..44296c306 --- /dev/null +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-schemas.ts @@ -0,0 +1,95 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +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 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") + .regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format') + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path) +}); + +const AwsParameterStoreSyncOptionsSchema = z.object({ + keyId: z + .string() + .regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID") + .min(1, "Invalid KMS Key ID") + .max(256, "Invalid KMS Key ID") + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.keyId), + tags: z + .object({ + key: z + .string() + .regex( + /^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, + "Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" + ) + .min(1, "Resource tag key required") + .max(128, "Resource tag key cannot exceed 128 characters"), + value: z + .string() + .regex( + /^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, + "Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" + ) + .max(256, "Resource tag value cannot exceed 256 characters") + }) + .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..5e8ce2bad --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts @@ -0,0 +1,137 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +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() + .regex( + /^[a-zA-Z0-9/_+=.@-]+$/, + "Secret name must contain only alphanumeric characters and the characters /_+=.@-" + ) + .min(1, "Secret name is required") + .max(256, "Secret name cannot exceed 256 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 AwsSecretsManagerSyncOptionsSchema = z.object({ + keyId: z + .string() + .regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID") + .min(1, "Invalid KMS Key ID") + .max(256, "Invalid KMS Key ID") + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.keyId), + tags: z + .object({ + key: z + .string() + .regex( + /^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, + "Invalid tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" + ) + .min(1, "Tag key required") + .max(128, "Tag key cannot exceed 128 characters"), + value: z + .string() + .regex( + /^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, + "Invalid tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" + ) + .max(256, "Tag value cannot exceed 256 characters") + }) + .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..b50d78bb2 --- /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.replace(/_/g, "-"); + 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.replace(/-/g, "_"); + 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.replace(/-/g, "_"); + + 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.replace(/-/g, "_"); + 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/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..cc2cd1fcf --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-dal.ts @@ -0,0 +1,196 @@ +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") + ); + + 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, + ...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 + }, + 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..006d033f5 --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -0,0 +1,21 @@ +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" +} + +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..6c8a6d4df --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -0,0 +1,239 @@ +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 { 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"; + +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 +}; + +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); + 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; + 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); + 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..cd4125e1b --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -0,0 +1,24 @@ +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" +}; + +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 +}; 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..8afcf6416 --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -0,0 +1,972 @@ +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" + >; + 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 + }); + } + + 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..5c4a7e850 --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -0,0 +1,534 @@ +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 + ) => { + const folders = await folderDAL.findByProjectId(projectId); + + // we prevent conflicting names within a project so this will only return one at most + const [secretSync] = await secretSyncDAL.find({ + name: syncName, + $in: { + folderId: folders.map((folder) => folder.id) + } + }); + + 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..2044c7c17 --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -0,0 +1,217 @@ +import { Job } from "bullmq"; + +import { TCreateAuditLogDTO } 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 { + 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 { + 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"; + +export type TSecretSync = + | TAwsParameterStoreSync + | TAwsSecretsManagerSync + | TGitHubSync + | TGcpSync + | TAzureKeyVaultSync + | TAzureAppConfigurationSync + | TDatabricksSync + | THumanitecSync; + +export type TSecretSyncWithCredentials = + | TAwsParameterStoreSyncWithCredentials + | TAwsSecretsManagerSyncWithCredentials + | TGitHubSyncWithCredentials + | TGcpSyncWithCredentials + | TAzureKeyVaultSyncWithCredentials + | TAzureAppConfigurationSyncWithCredentials + | TDatabricksSyncWithCredentials + | THumanitecSyncWithCredentials; + +export type TSecretSyncInput = + | TAwsParameterStoreSyncInput + | TAwsSecretsManagerSyncInput + | TGitHubSyncInput + | TGcpSyncInput + | TAzureKeyVaultSyncInput + | TAzureAppConfigurationSyncInput + | TDatabricksSyncInput + | THumanitecSyncInput; + +export type TSecretSyncListItem = + | TAwsParameterStoreSyncListItem + | TAwsSecretsManagerSyncListItem + | TGitHubSyncListItem + | TGcpSyncListItem + | TAzureKeyVaultSyncListItem + | TAzureAppConfigurationSyncListItem + | TDatabricksSyncListItem + | THumanitecSyncListItem; + +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; +}; + +type AuditLogInfo = Pick; + +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-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..b4619abd3 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 @@ -78,6 +78,12 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.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")) @@ -103,6 +109,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 + }) } ] }); @@ -221,7 +236,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 +250,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 +276,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 + }) } ] }); @@ -361,9 +393,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 +413,37 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.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") + ) .where((bd) => { const slugs = filters?.tagSlugs?.filter(Boolean); if (slugs && slugs.length > 0) { @@ -421,9 +483,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" }); @@ -541,14 +613,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 +642,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 + }) } ] }); 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 95d2cdbf4..0abf02aaa 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 @@ -5,7 +5,10 @@ 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"; @@ -54,11 +57,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 +93,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 +109,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 +129,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 +242,10 @@ export const fnSecretBulkUpdate = async ({ encryptedValue, reminderRepeatDays, folderId, - secretId + secretId, + userActorId, + identityActorId, + actorType }) ), tx @@ -231,7 +284,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 ({ @@ -365,9 +454,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; }; @@ -465,7 +553,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 +572,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); @@ -565,30 +653,56 @@ 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; + }, + 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, + + ...(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 d49a183ae..dc975854f 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,9 +1,26 @@ -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"; @@ -18,7 +35,9 @@ 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"; @@ -35,11 +54,13 @@ import { } from "./secret-v2-bridge-fns"; import { SecretOperations, + SecretUpdateMode, TBackFillSecretReferencesDTO, TCreateManySecretDTO, TCreateSecretDTO, TDeleteManySecretDTO, TDeleteSecretDTO, + TGetAccessibleSecretsDTO, TGetASecretDTO, TGetSecretReferencesTreeDTO, TGetSecretsDTO, @@ -63,7 +84,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 +101,7 @@ type TSecretV2BridgeServiceFactoryDep = { "insertV2Bridge" | "insertApprovalSecretV2Tags" >; snapshotService: Pick; + resourceMetadataDAL: Pick; }; export type TSecretV2BridgeServiceFactory = ReturnType; @@ -95,17 +123,20 @@ export const secretV2BridgeServiceFactory = ({ secretApprovalPolicyService, secretApprovalRequestDAL, secretApprovalRequestSecretDAL, - kmsService + kmsService, + resourceMetadataDAL }: 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,36 +150,42 @@ 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` }); - return { - operator: "and", - value: [ - { - operator: "eq", - field: "folderId", - value: folderId - }, - { - operator: "eq", - field: "key", - value: el.secretKey - } - ] - }; - }) - } - }); + 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` }); + + 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 !== @@ -164,15 +201,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; @@ -186,15 +220,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) @@ -232,7 +268,7 @@ export const secretV2BridgeServiceFactory = ({ const { secretName, type, ...inputSecretData } = inputSecret; ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -245,15 +281,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, @@ -271,21 +309,30 @@ 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; + }); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ secretPath, + orgId: actorOrgId, actorId, actor, projectId, @@ -293,11 +340,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 ({ @@ -308,15 +361,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" }); @@ -364,7 +419,7 @@ export const secretV2BridgeServiceFactory = ({ } ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -375,18 +430,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) + }) }) ); @@ -398,12 +457,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) + }) }) ); } @@ -414,12 +475,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); @@ -432,6 +494,8 @@ export const secretV2BridgeServiceFactory = ({ const updatedSecret = await secretDAL.transaction(async (tx) => fnSecretBulkUpdate({ folderId, + orgId: actorOrgId, + resourceMetadataDAL, inputSecrets: [ { filter: { id: secretId }, @@ -445,6 +509,7 @@ export const secretV2BridgeServiceFactory = ({ skipMultilineEncoding: inputSecret.skipMultilineEncoding, key: inputSecret.newSecretName || secretName, tags: inputSecret.tagIds, + secretMetadata, ...encryptedValue } } @@ -453,6 +518,10 @@ export const secretV2BridgeServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }) ); @@ -472,15 +541,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 ({ @@ -493,13 +582,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) @@ -525,7 +615,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, @@ -558,6 +648,7 @@ export const secretV2BridgeServiceFactory = ({ actorId, actor, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); } @@ -566,15 +657,33 @@ export const secretV2BridgeServiceFactory = ({ 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 + ); }; // get unique secrets count for multiple envs @@ -593,15 +702,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); @@ -639,15 +748,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; @@ -658,7 +767,13 @@ 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); @@ -677,18 +792,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, @@ -700,9 +825,10 @@ export const secretV2BridgeServiceFactory = ({ comment: secret.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() : "" - } - ) - ); + }, + secretValueHidden + ); + }); return decryptedSecrets; }; @@ -722,15 +848,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); @@ -750,7 +877,8 @@ export const secretV2BridgeServiceFactory = ({ projectId, folderMappings, filters: params, - userId: actorId + userId: actorId, + filterByAction: ProjectPermissionSecretActions.DescribeSecret }, permission ); @@ -765,21 +893,23 @@ export const secretV2BridgeServiceFactory = ({ projectId, actor, actorOrgId, + viewSecretValue, actorAuthMethod, includeImports, recursive, expandSecretReferences: shouldExpandSecretReferences, + throwOnMissingReadValuePermission = true, ...params }: TGetSecretsDTO) => { - 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); let paths: { folderId: string; path: string }[] = []; @@ -817,28 +947,83 @@ export const secretV2BridgeServiceFactory = ({ }); 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, @@ -846,15 +1031,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) { @@ -886,22 +1068,38 @@ export const secretV2BridgeServiceFactory = ({ 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 { @@ -910,6 +1108,76 @@ export const secretV2BridgeServiceFactory = ({ }; }; + 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 ({ actorId, actor, @@ -921,16 +1189,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) @@ -957,8 +1227,8 @@ 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({ @@ -980,66 +1250,91 @@ export const secretV2BridgeServiceFactory = ({ }) )); - 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 + ); } } } @@ -1049,7 +1344,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, @@ -1061,13 +1356,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 ({ @@ -1080,13 +1402,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) @@ -1108,7 +1431,7 @@ export const secretV2BridgeServiceFactory = ({ value: [ { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey }, { @@ -1134,7 +1457,7 @@ export const secretV2BridgeServiceFactory = ({ inputSecrets.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1165,7 +1488,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, @@ -1180,14 +1503,21 @@ 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 }) ); @@ -1198,16 +1528,34 @@ export const secretV2BridgeServiceFactory = ({ 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 ({ @@ -1217,203 +1565,339 @@ 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) + }) + ); + }); + + // 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 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 ({ @@ -1426,13 +1910,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) @@ -1454,7 +1939,7 @@ export const secretV2BridgeServiceFactory = ({ value: [ { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey }, { @@ -1474,7 +1959,7 @@ export const secretV2BridgeServiceFactory = ({ }); secretsToDelete.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1505,6 +1990,7 @@ export const secretV2BridgeServiceFactory = ({ actorId, secretPath, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); @@ -1512,13 +1998,32 @@ export const secretV2BridgeServiceFactory = ({ 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() : "" - }) - ); + return secretsDeleted.map((el) => { + const secretToDeleteMatch = secretsToDelete.find( + (i) => i.key === el.key && (i.type || SecretType.Shared) === el.type + ); + + 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 + ); + }); }; const getSecretVersions = async ({ @@ -1531,31 +2036,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 @@ -1568,13 +2104,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" }); @@ -1615,13 +2152,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) { @@ -1648,16 +2186,38 @@ 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) - }) - ); + 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) { @@ -1732,27 +2292,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( @@ -1806,11 +2356,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, @@ -1821,6 +2377,7 @@ export const secretV2BridgeServiceFactory = ({ skipMultilineEncoding: doc.skipMultilineEncoding, reminderNote: doc.reminderNote, reminderRepeatDays: doc.reminderRepeatDays, + secretMetadata: doc.secretMetadata, references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : [] }; }) @@ -1829,11 +2386,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: { @@ -1846,6 +2409,7 @@ export const secretV2BridgeServiceFactory = ({ encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, reminderNote: doc.reminderNote, + secretMetadata: doc.secretMetadata, reminderRepeatDays: doc.reminderRepeatDays, ...(doc.encryptedValue ? { @@ -1929,6 +2493,7 @@ export const secretV2BridgeServiceFactory = ({ await snapshotService.performSnapshot(destinationFolder.id); await secretQueueService.syncSecrets({ projectId, + orgId: actorOrgId, secretPath: destinationFolder.path, environmentSlug: destinationFolder.environment.slug, actorId, @@ -1940,6 +2505,7 @@ export const secretV2BridgeServiceFactory = ({ await snapshotService.performSnapshot(sourceFolder.id); await secretQueueService.syncSecrets({ projectId, + orgId: actorOrgId, secretPath: sourceFolder.path, environmentSlug: sourceFolder.environment.slug, actorId, @@ -1964,18 +2530,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) @@ -1996,17 +2563,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() : ""; @@ -2016,26 +2582,125 @@ 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 + }: TGetAccessibleSecretsDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath + }); + + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) return { secrets: [] }; + + const secrets = await secretDAL.findByFolderIds([folder.id]); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const decryptedSecrets = secrets + .filter((el) => { + if ( + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ) { + return false; + } + + if (filterByAction === ProjectPermissionSecretActions.ReadValue) { + return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + 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, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + secretValueHidden + ); + }); + + return { + secrets: decryptedSecrets + }; + }; + return { createSecret, deleteSecret, @@ -2052,6 +2717,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..7f415bff8 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,6 +128,7 @@ export type TUpdateManySecretDTO = Omit & { secretPath: string; projectId: string; environment: string; + mode: SecretUpdateMode; secrets: { secretKey: string; newSecretName?: string; @@ -112,6 +138,8 @@ export type TUpdateManySecretDTO = Omit & { 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; }; @@ -293,7 +339,9 @@ export type TFindSecretsByFolderIdsFilter = { orderDirection?: OrderByDirection; search?: string; tagSlugs?: string[]; + metadataFilter?: { key?: string; value?: string }[]; includeTagsInSearch?: boolean; + keys?: string[]; }; export type TGetSecretsRawByFolderMappingsDTO = { @@ -301,4 +349,12 @@ 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; + 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..aa8cec2bc 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -1,8 +1,8 @@ /* eslint-disable no-await-in-loop */ -import { subject } from "@casl/ability"; import path from "path"; import { + ActionProjectType, SecretEncryptionAlgo, SecretKeyEncoding, SecretType, @@ -11,17 +11,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 +34,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 +51,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 +179,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; @@ -338,6 +343,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD export const decryptSecretRaw = ( secret: TSecrets & { + secretValueHidden: boolean; workspace: string; environment: string; secretPath: string; @@ -356,12 +362,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 +387,7 @@ export const decryptSecretRaw = ( secretPath: secret.secretPath, workspace: secret.workspace, environment: secret.environment, + secretValueHidden: secret.secretValueHidden, secretValue, secretComment, version: secret.version, @@ -573,6 +582,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 +755,8 @@ export const createManySecretsRawFnFactory = ({ secretVersionV2BridgeDAL, secretV2BridgeDAL, secretVersionTagV2BridgeDAL, - kmsService + kmsService, + resourceMetadataDAL }: TCreateManySecretsRawFnFactory) => { const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL); const createManySecretsRawFn = async ({ @@ -756,7 +767,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 +821,9 @@ export const createManySecretsRawFnFactory = ({ tagIds: el.tags })), folderId, + orgId: project.orgId, secretDAL: secretV2BridgeDAL, + resourceMetadataDAL, secretVersionDAL: secretVersionV2BridgeDAL, secretTagDAL, secretVersionTagDAL: secretVersionTagV2BridgeDAL, @@ -905,6 +918,7 @@ export const updateManySecretsRawFnFactory = ({ secretVersionTagV2BridgeDAL, secretVersionV2BridgeDAL, secretV2BridgeDAL, + resourceMetadataDAL, kmsService }: TUpdateManySecretsRawFnFactory) => { const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL); @@ -916,6 +930,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 +999,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 +1155,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..76a486b46 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 }); /** @@ -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,16 @@ export const secretQueueFactory = ({ } } ); + + 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 +711,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 +746,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 +799,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 +973,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 +1008,7 @@ export const secretQueueFactory = ({ isSynced: false, lastSyncJobId: job?.id ?? "", lastUsed: new Date(), - syncMessage: message + syncMessage: errorLog } } }); @@ -957,13 +1020,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 +1064,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 +1094,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 +1512,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..13a5c9736 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,118 @@ 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 + }: 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 + }); + + return secrets; + }; + const getSecretsRaw = async ({ projectId, path, @@ -1121,11 +1353,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 +1370,8 @@ export const secretServiceFactory = ({ actorId, actor, actorOrgId, + viewSecretValue, + throwOnMissingReadValuePermission, environment, path, recursive, @@ -1144,6 +1380,7 @@ export const secretServiceFactory = ({ tagSlugs, ...paramsV2 }); + return { secrets, imports }; } @@ -1153,6 +1390,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 +1409,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 +1433,7 @@ export const secretServiceFactory = ({ const importedEntries = decryptedImportSecrets.reduce( ( accum: { + secretValueHidden: boolean; secretKey: string; secretPath: string; workspace: string; @@ -1226,6 +1477,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 +1494,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 +1515,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,6 +1552,7 @@ export const secretServiceFactory = ({ includeImports, actorAuthMethod, path, + viewSecretValue, actorOrgId, actor, actorId, @@ -1316,6 +1583,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 +1602,10 @@ export const secretServiceFactory = ({ decryptedSecret.secretValue = expandedSecretValue || ""; } - return decryptedSecret; + return { + secretMetadata: undefined, + ...decryptedSecret + }; }; const createSecretRaw = async ({ @@ -1352,9 +1623,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 +1661,8 @@ export const secretServiceFactory = ({ secretValue, tagIds, reminderNote: secretReminderNote, - reminderRepeatDays: secretReminderRepeatDays + reminderRepeatDays: secretReminderRepeatDays, + secretMetadata } ] } @@ -1402,7 +1685,8 @@ export const secretServiceFactory = ({ tagIds, secretReminderNote, skipMultilineEncoding, - secretReminderRepeatDays + secretReminderRepeatDays, + secretMetadata }); return { secret, type: SecretProtectionType.Direct as const }; } @@ -1472,7 +1756,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 +1785,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 +1824,8 @@ export const secretServiceFactory = ({ secretValue, tagIds, reminderNote: secretReminderNote, - reminderRepeatDays: secretReminderRepeatDays + reminderRepeatDays: secretReminderRepeatDays, + secretMetadata } ] } @@ -1544,7 +1849,8 @@ export const secretServiceFactory = ({ secretName, newSecretName, metadata, - secretValue + secretValue, + secretMetadata }); return { type: SecretProtectionType.Direct as const, secret }; } @@ -1742,7 +2048,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 +2082,8 @@ export const secretServiceFactory = ({ secretComment: el.secretComment, metadata: el.metadata, skipMultilineEncoding: el.skipMultilineEncoding, - secretKey: el.secretKey + secretKey: el.secretKey, + secretMetadata: el.secretMetadata })) } }); @@ -1838,7 +2161,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 +2175,7 @@ export const secretServiceFactory = ({ actorOrgId, actorAuthMethod, secretPath, + mode = SecretUpdateMode.FailOnNotFound, secrets: inputSecrets = [] }: TUpdateManySecretRawDTO) => { if (!projectSlug && !optionalProjectId) @@ -1870,6 +2194,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 +2225,8 @@ export const secretServiceFactory = ({ secretValue: el.secretValue, secretComment: el.secretComment, skipMultilineEncoding: el.skipMultilineEncoding, - secretKey: el.secretKey + secretKey: el.secretKey, + secretMetadata: el.secretMetadata })) } }); @@ -1900,7 +2240,8 @@ export const secretServiceFactory = ({ actorOrgId, actor, actorId, - secrets: inputSecrets + secrets: inputSecrets, + mode }); return { type: SecretProtectionType.Direct as const, secrets }; } @@ -2109,30 +2450,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 +2521,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 +2603,7 @@ export const secretServiceFactory = ({ await secretQueueService.syncSecrets({ secretPath, projectId: project.id, + orgId: project.orgId, environmentSlug: environment, excludeReplication: true }); @@ -2252,16 +2627,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 +2713,7 @@ export const secretServiceFactory = ({ await secretQueueService.syncSecrets({ secretPath, projectId: project.id, + orgId: project.orgId, environmentSlug: environment, excludeReplication: true }); @@ -2357,13 +2734,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 +2803,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 +2819,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 +2855,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 +2866,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 +3189,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 +3201,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 +3223,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 +3252,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 +3293,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..6a5c097ed 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,44 @@ export enum SecretsOrderBy { Name = "name" // "key" for secrets but using name for use across resources } +export type TGetAccessibleSecretsDTO = { + secretPath: string; + environment: string; + 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 +227,10 @@ export type TGetASecretRawDTO = { projectId?: string; } & Omit; +export type TGetASecretByIdRawDTO = { + secretId: string; +} & Omit; + export type TCreateSecretRawDTO = TProjectPermission & { secretName: string; secretPath: string; @@ -210,6 +242,7 @@ export type TCreateSecretRawDTO = TProjectPermission & { skipMultilineEncoding?: boolean; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; + secretMetadata?: ResourceMetadataDTO; }; export type TUpdateSecretRawDTO = TProjectPermission & { @@ -227,6 +260,7 @@ export type TUpdateSecretRawDTO = TProjectPermission & { metadata?: { source?: string; }; + secretMetadata?: ResourceMetadataDTO; }; export type TDeleteSecretRawDTO = TProjectPermission & { @@ -247,6 +281,7 @@ export type TCreateManySecretRawDTO = Omit & { secretComment?: string; skipMultilineEncoding?: boolean; tagIds?: string[]; + secretMetadata?: ResourceMetadataDTO; metadata?: { source?: string; }; @@ -258,6 +293,7 @@ export type TUpdateManySecretRawDTO = Omit & { projectId?: string; projectSlug?: string; environment: string; + mode: SecretUpdateMode; secrets: { secretKey: string; newSecretName?: string; @@ -265,6 +301,7 @@ export type TUpdateManySecretRawDTO = Omit & { secretComment?: string; skipMultilineEncoding?: boolean; tagIds?: string[]; + secretMetadata?: ResourceMetadataDTO; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; }[]; @@ -292,7 +329,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 +427,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 +464,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 +504,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-service.ts b/backend/src/services/service-token/service-token-service.ts index fe2c1c0d2..9b87c29f8 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -3,8 +3,13 @@ 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"; @@ -54,18 +59,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 +115,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 +150,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"]] }); diff --git a/backend/src/services/slack/slack-fns.ts b/backend/src/services/slack/slack-fns.ts index 919f9de05..f92f96a24 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>.`; diff --git a/backend/src/services/slack/slack-types.ts b/backend/src/services/slack/slack-types.ts index a1914eee2..a92ba4e8b 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[]; }; } | { diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 1f38babb3..68e0ecd22 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -30,14 +30,17 @@ 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" } export enum SmtpHost { @@ -53,6 +56,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 +87,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..3c0811a1c 100644 --- a/backend/src/services/smtp/templates/accessApprovalRequest.handlebars +++ b/backend/src/services/smtp/templates/accessApprovalRequest.handlebars @@ -45,6 +45,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/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/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/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/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/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 8ef998ac3..317348cac 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,27 +1,46 @@ 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; kmsRootConfigDAL: TKmsRootConfigDALFactory; @@ -48,12 +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 @@ -83,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 ( @@ -104,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" }); @@ -247,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); @@ -353,10 +574,15 @@ export const superAdminServiceFactory = ({ initServerCfg, updateServerCfg, adminSignUp, + bootstrapInstance, getUsers, deleteUser, + getIdentities, getAdminSlackConfig, updateRootEncryptionStrategy, - getConfiguredEncryptionStrategies + 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..786afb9d2 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -13,7 +13,9 @@ 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" } export type TSecretModifiedEvent = { @@ -120,6 +122,23 @@ 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 TPostHogEvent = { distinctId: string } & ( | TSecretModifiedEvent | TAdminInitEvent @@ -130,4 +149,6 @@ export type TPostHogEvent = { distinctId: string } & ( | TIntegrationCreatedEvent | TProjectCreateEvent | TTelemetryInstanceStatsEvent + | TSecretRequestCreatedEvent + | TSecretRequestDeletedEvent ); 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..b2d0c83ad 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,21 +12,27 @@ 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.4.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 ) @@ -55,16 +63,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 +89,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 +110,27 @@ 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..5f1f369bb 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.4.8 h1:aphRnaauC5//PkP1ZbY9RSK2RiT1LjPS5o4CbX0x5OQ= +github.com/infisical/go-sdk v0.4.8/go.mod h1:bMO9xSaBeXkDBhTIM4FkkREAfw2V8mv5Bm7lvo4+uDk= +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= @@ -847,4 +858,4 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= \ No newline at end of file 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 adf7a814d..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"` @@ -138,6 +142,7 @@ type GetOrganizationsResponse struct { type SelectOrganizationResponse struct { 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/bootstrap.go b/cli/packages/cmd/bootstrap.go new file mode 100644 index 000000000..008debbac --- /dev/null +++ b/cli/packages/cmd/bootstrap.go @@ -0,0 +1,104 @@ +/* +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/go-resty/resty/v2" + "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 := resty.New(). + 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..9e0ea1334 --- /dev/null +++ b/cli/packages/cmd/dynamic_secrets.go @@ -0,0 +1,571 @@ +/* +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/go-resty/resty/v2" + "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 := resty.New() + + 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) + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + }) + 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 := resty.New() + + 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) + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + }) + 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 := resty.New() + + 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) + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + }) + 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 := resty.New() + + 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) + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + }) + 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 := resty.New() + + 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) + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + }) + 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..df6bfcc60 100644 --- a/cli/packages/cmd/init.go +++ b/cli/packages/cmd/init.go @@ -41,7 +41,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") } @@ -79,13 +79,14 @@ var initCmd = &cobra.Command{ if tokenResponse.MfaEnabled { i := 1 for i < 6 { - mfaVerifyCode := askForMFACode() - + mfaVerifyCode := askForMFACode(tokenResponse.MfaMethod) + httpClient := resty.New() 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 +100,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..81bb0c754 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -154,6 +154,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) @@ -198,7 +200,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 +218,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 +238,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 +247,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 +315,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,7 +357,7 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) { if loginTwoResponse.MfaEnabled { i := 1 for i < 6 { - mfaVerifyCode := askForMFACode() + mfaVerifyCode := askForMFACode("email") httpClient := resty.New() httpClient.SetAuthToken(loginTwoResponse.Token) @@ -526,13 +540,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" ) @@ -756,13 +809,14 @@ 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.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 +871,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() diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index c533f3415..04af9cce8 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -54,7 +54,7 @@ func init() { 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..8e9f5d63b 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -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) @@ -143,7 +143,15 @@ var secretsSetCmd = &cobra.Command{ 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,18 @@ var secretsSetCmd = &cobra.Command{ util.HandleError(err, "Unable to parse secret type") } + 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 +207,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]") } @@ -206,7 +219,7 @@ var secretsSetCmd = &cobra.Command{ secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{ Type: "", Token: loggedInUserDetails.UserCredentials.JTWToken, - }) + }, file) } if err != nil { @@ -278,7 +291,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 +704,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..d7c1f1e26 --- /dev/null +++ b/cli/packages/cmd/ssh.go @@ -0,0 +1,609 @@ +/* +Copyright (c) 2023 Infisical Inc. +*/ +package cmd + +import ( + "context" + "fmt" + "net" + "os" + "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/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 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 == false { + 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") + } + } + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + }) + 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 + } + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + }) + 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 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) + rootCmd.AddCommand(sshCmd) +} diff --git a/cli/packages/cmd/tokens.go b/cli/packages/cmd/tokens.go index e2851f88f..531e622e9 100644 --- a/cli/packages/cmd/tokens.go +++ b/cli/packages/cmd/tokens.go @@ -41,7 +41,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") 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..d0246e3bc --- /dev/null +++ b/cli/packages/gateway/gateway.go @@ -0,0 +1,362 @@ +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/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 := resty.New() + 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/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..03722dc41 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -55,7 +55,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 +75,20 @@ 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(). 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..4715c71c3 100644 --- a/cli/packages/util/folders.go +++ b/cli/packages/util/folders.go @@ -20,7 +20,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 } @@ -177,7 +177,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 @@ -224,7 +224,7 @@ func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder, RequireLogin() RequireLocalWorkspaceFile() - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) if err != nil { return nil, err diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 11a1e3e0a..d3eb0cc34 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -245,8 +245,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..02079d656 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -17,6 +17,7 @@ import ( "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) { @@ -246,7 +247,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 { @@ -564,7 +565,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") 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/test/.snapshots/test-TestUniversalAuth_SecretsGetWrongEnvironment b/cli/test/.snapshots/test-TestUniversalAuth_SecretsGetWrongEnvironment index ff7925d75..b447d947e 100644 --- a/cli/test/.snapshots/test-TestUniversalAuth_SecretsGetWrongEnvironment +++ b/cli/test/.snapshots/test-TestUniversalAuth_SecretsGetWrongEnvironment @@ -1,4 +1,4 @@ -error: CallGetRawSecretsV3: Unsuccessful response [GET https://app.infisical.com/api/v3/secrets/raw?environment=invalid-env&expandSecretReferences=true&include_imports=true&recursive=true&secretPath=%2F&workspaceId=bef697d4-849b-4a75-b284-0922f87f8ba2] [status-code=404] [response={"statusCode":404,"message":"Environment with slug 'invalid-env' in project with ID bef697d4-849b-4a75-b284-0922f87f8ba2 not found","error":"NotFound"}] +error: CallGetRawSecretsV3: Unsuccessful response [GET https://app.infisical.com/api/v3/secrets/raw?environment=invalid-env&expandSecretReferences=true&include_imports=true&recursive=true&secretPath=%2F&workspaceId=bef697d4-849b-4a75-b284-0922f87f8ba2] [status-code=404] [response={"error":"NotFound","message":"Environment with slug 'invalid-env' in project with ID bef697d4-849b-4a75-b284-0922f87f8ba2 not found","statusCode":404}] If this issue continues, get support at https://infisical.com/slack diff --git a/cli/test/.snapshots/test-testUserAuth_SecretsGetAllWithoutConnection b/cli/test/.snapshots/test-testUserAuth_SecretsGetAllWithoutConnection index 71a189a65..2ca9d13ad 100644 --- a/cli/test/.snapshots/test-testUserAuth_SecretsGetAllWithoutConnection +++ b/cli/test/.snapshots/test-testUserAuth_SecretsGetAllWithoutConnection @@ -1,4 +1,4 @@ -Warning: Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug +Warning: Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug ┌───────────────┬──────────────┬─────────────┐ │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ ├───────────────┼──────────────┼─────────────┤ diff --git a/cli/test/helper.go b/cli/test/helper.go index 819f4c4c9..21bd261df 100644 --- a/cli/test/helper.go +++ b/cli/test/helper.go @@ -1,6 +1,7 @@ package tests import ( + "encoding/json" "fmt" "log" "os" @@ -41,11 +42,12 @@ var creds = Credentials{ func ExecuteCliCommand(command string, args ...string) (string, error) { cmd := exec.Command(command, args...) output, err := cmd.CombinedOutput() + if err != nil { - fmt.Println(fmt.Sprint(err) + ": " + string(output)) - return strings.TrimSpace(string(output)), err + fmt.Println(fmt.Sprint(err) + ": " + FilterRequestID(strings.TrimSpace(string(output)))) + return FilterRequestID(strings.TrimSpace(string(output))), err } - return strings.TrimSpace(string(output)), nil + return FilterRequestID(strings.TrimSpace(string(output))), nil } func SetupCli() { @@ -67,3 +69,34 @@ func SetupCli() { } } + +func FilterRequestID(input string) string { + // Find the JSON part of the error message + start := strings.Index(input, "{") + end := strings.LastIndex(input, "}") + 1 + + if start == -1 || end == -1 { + return input + } + + jsonPart := input[:start] // Pre-JSON content + + // Parse the JSON object + var errorObj map[string]interface{} + if err := json.Unmarshal([]byte(input[start:end]), &errorObj); err != nil { + return input + } + + // Remove requestId field + delete(errorObj, "requestId") + delete(errorObj, "reqId") + + // Convert back to JSON + filtered, err := json.Marshal(errorObj) + if err != nil { + return input + } + + // Reconstruct the full string + return jsonPart + string(filtered) + input[end:] +} diff --git a/cli/test/secrets_test.go b/cli/test/secrets_test.go index f11392f52..f7f0f13ff 100644 --- a/cli/test/secrets_test.go +++ b/cli/test/secrets_test.go @@ -3,7 +3,6 @@ package tests import ( "testing" - "github.com/Infisical/infisical-merge/packages/util" "github.com/bradleyjkemp/cupaloy/v2" ) @@ -96,28 +95,29 @@ func TestUserAuth_SecretsGetAll(t *testing.T) { // testUserAuth_SecretsGetAllWithoutConnection(t) } -func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) { - originalConfigFile, err := util.GetConfigFile() - if err != nil { - t.Fatalf("error getting config file") - } - newConfigFile := originalConfigFile +// disabled for the time being +// func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) { +// originalConfigFile, err := util.GetConfigFile() +// if err != nil { +// t.Fatalf("error getting config file") +// } +// newConfigFile := originalConfigFile - // set it to a URL that will always be unreachable - newConfigFile.LoggedInUserDomain = "http://localhost:4999" - util.WriteConfigFile(&newConfigFile) +// // set it to a URL that will always be unreachable +// newConfigFile.LoggedInUserDomain = "http://localhost:4999" +// util.WriteConfigFile(&newConfigFile) - // restore config file - defer util.WriteConfigFile(&originalConfigFile) +// // restore config file +// defer util.WriteConfigFile(&originalConfigFile) - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent") - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } +// output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent") +// if err != nil { +// t.Fatalf("error running CLI command: %v", err) +// } - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} +// // Use cupaloy to snapshot test the output +// err = cupaloy.Snapshot(output) +// if err != nil { +// t.Fatalf("snapshot failed: %v", err) +// } +// } diff --git a/company/documentation/engineering/how-to-write-design-doc.mdx b/company/documentation/engineering/how-to-write-design-doc.mdx new file mode 100644 index 000000000..7f6a49e47 --- /dev/null +++ b/company/documentation/engineering/how-to-write-design-doc.mdx @@ -0,0 +1,67 @@ +--- +title: "How to write a design document" +sidebarTitle: "Writing Design Docs" +description: "Learn how to write a design document at Infisical" +--- + +## **Why write a design document?** + +Writing a design document helps you efficiently solve broad, complex engineering problems at Infisical. While planning is important, we are a startup, so speed and urgency should be your top of mind. Keep the process lightweight and time boxed so that we can get the most out of it. + +**Writing a design will help you:** + +- **Understand the problem space:** Deeply understand the problem you’re solving to make sure it is well scoped. +- **Stay on the right path:** Without proper planning, you risk cycling between partial implementation and replanning, encountering roadblocks that force you back to square one. A solid plan minimizes wasted engineering hours. +- **An opportunity to collaborate:** Bring relevant engineers into the discussion to develop well-thought-out solutions and catch potential issues you might have overlooked. +- **Faster implementation:** A well-thought-out plan will help you catch roadblocks early and ship quickly because you know exactly what needs to get implemented. + +**When to write a design document:** + +- **Write a design doc**: If the feature is not well defined, high-security, or will take more than **1 full engineering week** to build. +- **Skip the design doc**: For small, straightforward features that can be built quickly with informal discussions. + +If you are unsure when to create a design doc, chat with @maidul. + +## **What to Include in your Design Document** + +Every feature/problem is unique, but your design docs should generally include the following sections. If you need to include additional sections, feel free to do so. + +1. **Title** + - A descriptive title. + - Name of document owner and name of reviewer(s). +2. **Overview** + - A high-level summary of the problem and proposed solution. Keep it brief (max 3 paragraphs). +3. **Context** + - Explain the problem’s background, why it’s important to solve now, and any constraints (e.g., technical, sales, or timeline-related). What do we get out of solving this problem? (needed to close a deal, scale, performance, etc.). +4. **Solution** + - Provide a big-picture explanation of the solution, followed by detailed technical architecture. + - Use diagrams/charts where needed. + - Write clearly so that another engineer could implement the solution in your absence. +5. **Milestones** + - Break the project into phases with clear start and end dates estimates. Use a table or bullet points. +6. **FAQ** + - Common questions or concerns someone might have while reading your document that can be quickly addressed. + + +## **How to Write a Design Doc** + +- **Keep it Simple**: Use clear, simple language. Opt for short sentences, bullet points, and concrete examples over fluff writing. +- **Use Visuals**: Add diagrams and charts for clarity to convey your ideas. +- **Make it Self-Explanatory**: Ensure that anyone reading the document can understand and implement the plan without needing additional context. + +Before sharing your design docs with others, review your design doc as if you were a teammate seeing it for the first time. Anticipate questions and address them. + + +## **Process from start to finish** + +1. **Research/Discuss** + - Before you start writing, take some time to research and get a solid understanding of the problem space. Look into how other well-established companies are tackling similar challenges, if they are. + Talk through the problem and your initial solution with other engineers on the team—bounce ideas around and get their feedback. If you have ideas on how the system could if implemented in Infisical, would it effect any downstream features/systems, etc? + + Once you’ve got a general direction, you might need to test a some theories. This is where quick proof of concepts (POCs) come in handy, but don’t get too caught up in the details. The goal of a POC is simply to validate a core idea or concept so you can get to the rest of your planning. +2. **Write the Doc** + - Based on your research/discussions, write the design doc and include all relevant sections. Your goal is to come up with a convincing plan on why this is the correct why to solve the problem at hand. +3. **Assign Reviewers** + - Ask a relevant engineer(s) to review your document. Their role is to identify blind spots, challenge assumptions, and ensure everything is clear. Once you and the reviewer are on the same page on the approach, update the document with any missing details they brought up. +4. **Team Review and Feedback** + - Invite the relevant engineers to a design doc review meeting and give them 10-15 minutes to read through the document. After everyone has had a chance to review it, open the floor up for discussion. Address any feedback or concerns raised during this meeting. If significant points were overlooked during your initial planning, you may need to revisit the drawing board. Your goal is to think about the feature holistically and minimize the need for drastic changes to your design doc later on. \ No newline at end of file diff --git a/company/documentation/engineering/oncall-summery-template.mdx b/company/documentation/engineering/oncall-summery-template.mdx new file mode 100644 index 000000000..523f71e00 --- /dev/null +++ b/company/documentation/engineering/oncall-summery-template.mdx @@ -0,0 +1,22 @@ +--- +title: "On call summary template" +sidebarTitle: "Summary template" +--- + +```plain +Date: MM/DD/YY-MM/DD/YY + +Notable incidents: +- []
+ - 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..680a655d8 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,9 +128,6 @@ 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 @@ -159,6 +177,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 +195,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/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/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/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/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/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/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/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/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..a20f52171 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -4,6 +4,39 @@ title: "Changelog" The changelog below reflects new product developments and updates on a monthly basis. +## 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. 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..d1f4baa9f 100644 --- a/docs/cli/commands/secrets.mdx +++ b/docs/cli/commands/secrets.mdx @@ -219,6 +219,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/overview.mdx b/docs/cli/overview.mdx index 397d1f474..c2e13ee1c 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -1,9 +1,9 @@ --- 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 @@ -127,4 +127,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/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/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/role-based-access-controls.mdx b/docs/documentation/platform/access-controls/role-based-access-controls.mdx index 98a2e4659..9d1170f69 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). diff --git a/docs/documentation/platform/admin-panel/server-admin.mdx b/docs/documentation/platform/admin-panel/server-admin.mdx index ddcf448a6..137cdde59 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. ![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. +### Notices + +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..aca42c5c4 100644 --- a/docs/documentation/platform/dynamic-secrets/mssql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mssql.mdx @@ -28,7 +28,7 @@ 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) @@ -105,12 +105,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..da39c0a56 100644 --- a/docs/documentation/platform/dynamic-secrets/mysql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mysql.mdx @@ -27,7 +27,7 @@ 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) @@ -102,12 +102,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..e8fa86028 100644 --- a/docs/documentation/platform/dynamic-secrets/oracle.mdx +++ b/docs/documentation/platform/dynamic-secrets/oracle.mdx @@ -27,7 +27,7 @@ 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) @@ -102,12 +102,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..5216d87af 100644 --- a/docs/documentation/platform/dynamic-secrets/postgresql.mdx +++ b/docs/documentation/platform/dynamic-secrets/postgresql.mdx @@ -28,7 +28,7 @@ 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) @@ -105,12 +105,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..598a3a32b 100644 --- a/docs/documentation/platform/kms-configuration/aws-kms.mdx +++ b/docs/documentation/platform/kms-configuration/aws-kms.mdx @@ -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 index 4f9efe49f..633377b3d 100644 --- a/docs/documentation/platform/kms/hsm-integration.mdx +++ b/docs/documentation/platform/kms/hsm-integration.mdx @@ -66,7 +66,7 @@ For organizations that work with US government agencies, FIPS compliance is almo - Are you using Docker? If you are using Docker, please follow the instructions in the [Using HSM's with Docker](#using-hsms-with-docker) section. + 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: @@ -94,165 +94,447 @@ For organizations that work with US government agencies, FIPS compliance is almo -## Using HSMs with Docker -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. +## Using HSMs In Your Deployment + - - - - 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`. + + 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. - ```bash - mkdir /etc/luna-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`. - 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. + ```bash + mkdir /etc/luna-docker + ``` - 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. + 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. - ```bash - cp -r / /etc/luna-docker - ``` + 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. + + 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. - An example config file will look like this: + 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. - ```Chrystoki.conf - Chrystoki2 = { - # This path points to the mounted path, /usr/safenet/lunaclient - LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so; - } + An example config file will look like this: - Luna = { - DefaultTimeOut = 500000; - PEDTimeout1 = 100000; - PEDTimeout2 = 200000; - PEDTimeout3 = 20000; - KeypairGenTimeOut = 2700000; - CloningCommandTimeOut = 300000; - CommandTimeOutPedSet = 720000; - } + ```Chrystoki.conf + Chrystoki2 = { + # This path points to the mounted path, /usr/safenet/lunaclient + LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so; + } - CardReader = { - LunaG5Slots = 0; - RemoteCommand = 1; - } + Luna = { + DefaultTimeOut = 500000; + PEDTimeout1 = 100000; + PEDTimeout2 = 200000; + PEDTimeout3 = 20000; + KeypairGenTimeOut = 2700000; + CloningCommandTimeOut = 300000; + CommandTimeOutPedSet = 720000; + } - 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; + 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; - } + 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; - } - ``` + 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. - + 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. + + 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. + 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`. - + + 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 - ``` + ```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). + 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. + + + 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. 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/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/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/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-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.mdx b/docs/documentation/platform/ssh.mdx new file mode 100644 index 000000000..16faf1267 --- /dev/null +++ b/docs/documentation/platform/ssh.mdx @@ -0,0 +1,210 @@ +--- +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 a 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 a 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 a 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 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 in conjunction 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 a SSH project in the SSH tab of your organization. + + ![ssh project create](/images/platform/ssh/ssh-project.png) + + 1.2. Next, create a CA in the **Certificate Authorities** tab of the + project. + + ![ssh create ca](/images/platform/ssh/ssh-create-ca-1.png) + + ![ssh create ca popup](/images/platform/ssh/ssh-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 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`. + + 1.3. 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 year. + + ![ssh create template](/images/platform/ssh/ssh-create-template-1.png) + + ![ssh create template popup](/images/platform/ssh/ssh-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. + - Max TTL: The maximum TTL for certificates issued under this template. + - Allow User Certificates: Whether or not to allow issuance of user certificates. + - Allow Host Certificates: Whether or not to allow issuance of host certificates. + - 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. + + 1.4. Finally, add the user(s) you wish to be able to request a SSH certificate to the SSH project through the **Access Control** tab. + + + + + 2.1. Begin by downloading the CA's public key from the CA's details section. + + ![ssh ca public key](/images/platform/ssh/ssh-ca-public-key.png) + + + The CA's public key can also be retrieved programmatically via API by making a `GET` request to the `/ssh/ca//public-key` endpoint. + + + 2.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`. + + 2.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 + ``` + + 2.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 + +We show how to obtain a 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. + \ No newline at end of file diff --git a/docs/documentation/platform/sso/auth0-saml.mdx b/docs/documentation/platform/sso/auth0-saml.mdx new file mode 100644 index 000000000..b77c733f7 --- /dev/null +++ b/docs/documentation/platform/sso/auth0-saml.mdx @@ -0,0 +1,93 @@ +--- +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. + + + + + + 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..21236cd5b 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` diff --git a/docs/documentation/platform/sso/google-saml.mdx b/docs/documentation/platform/sso/google-saml.mdx index 4f31bffb1..7e47d1137 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. diff --git a/docs/documentation/platform/sso/jumpcloud.mdx b/docs/documentation/platform/sso/jumpcloud.mdx index ce89b8e0d..ec876b9da 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. 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..4f5bc689e --- /dev/null +++ b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx @@ -0,0 +1,113 @@ +--- +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**, **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-saml.mdx b/docs/documentation/platform/sso/keycloak-saml.mdx index 53f47f1ae..86352bb9d 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) diff --git a/docs/documentation/platform/sso/okta.mdx b/docs/documentation/platform/sso/okta.mdx index 9a1d4aa2f..9f28f4c4e 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) 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/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/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/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/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/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/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-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-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/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/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-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-project.png b/docs/images/platform/ssh/ssh-project.png new file mode 100644 index 000000000..0b57f9245 Binary files /dev/null and b/docs/images/platform/ssh/ssh-project.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/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/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/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/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/aws.mdx b/docs/integrations/app-connections/aws.mdx new file mode 100644 index 000000000..cb8a5bce0 --- /dev/null +++ b/docs/integrations/app-connections/aws.mdx @@ -0,0 +1,357 @@ +--- +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. 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 **Organization ID** to further enhance security. + + + + 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/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/overview.mdx b/docs/integrations/app-connections/overview.mdx new file mode 100644 index 000000000..64f3616de --- /dev/null +++ b/docs/integrations/app-connections/overview.mdx @@ -0,0 +1,77 @@ +--- +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. + \ No newline at end of file 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-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index 64df1df32..1f3a0da1f 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -17,6 +17,7 @@ Prerequisites: 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. @@ -40,9 +41,10 @@ The following steps are for instances not deployed on AWS 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) +![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**. @@ -59,6 +61,7 @@ The following steps are for instances not deployed on AWS 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. + @@ -89,11 +92,13 @@ The following steps are for instances not deployed on AWS ] } ``` + - - ![Copy IAM Role ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png) - + + ![Copy IAM Role + ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png) + 1. Navigate to your project's integrations tab in Infisical. @@ -104,6 +109,7 @@ The following steps are for instances not deployed on AWS ![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: @@ -127,6 +133,12 @@ The following steps are for instances not deployed on AWS Optionally, you can add tags or specify the encryption key of all the secrets created via this integration: + + The sync mode for AWS tags. The supported options are `Secret Metadata` and `Custom`. If `Secret Metadata` is selected, + the metadata of the Infisical secrets are used as tags in AWS. If custom is selected, then the key/value of the **Secret Tag** field is used. `Secret Metadata` mode + is only supported for one-to-one integrations. + + 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. diff --git a/docs/integrations/cloud/azure-app-configuration.mdx b/docs/integrations/cloud/azure-app-configuration.mdx index 249325704..a96bec7b2 100644 --- a/docs/integrations/cloud/azure-app-configuration.mdx +++ b/docs/integrations/cloud/azure-app-configuration.mdx @@ -29,6 +29,36 @@ description: "How to sync secrets from Infisical to Azure App Configuration" ![integrations](../../images/integrations/azure-app-configuration/create-integration-form.png) Press create integration to start syncing secrets to Azure App Configuration. + + + The Azure App Configuration integration 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 Reader** role for read-only access or **App Configuration Data Contributor** role for read/write access. + + + + + + #### Azure references + When adding secrets in Infisical that reference Azure Key Vault secrets, Infisical will automatically sets the content type to `application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8` in Azure App Configuration. + The following reference formats are automatically detected when added on Infisical's side: + - `{ "uri": "https://my-key-vault.vault.azure.net/secrets/my-secret" }` + - `https://my-key-vault.vault.azure.net/secrets/my-secret` + + #### Azure Labels + You can sync secrets from Infisical to Azure with custom labels by enabling the `Use Labels` option during setup: + + **When enabled**: Secrets will be pushed to Azure with your specified label + + **When disabled**: Secrets will be pushed with an empty (null) label + + + If you have set the initial sync to `import` have behavior, the label selection affects which secrets are imported from Azure: + - With `Use Labels` disabled: Only secrets with empty labels are imported on initial sync + - With `Use Labels` enabled: Only secrets matching your specified label are imported on initial sync + 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..ae04fda88 100644 --- a/docs/integrations/cloud/azure-key-vault.mdx +++ b/docs/integrations/cloud/azure-key-vault.mdx @@ -17,6 +17,10 @@ description: "How to sync secrets from Infisical to Azure Key Vault" ![integrations](../../images/integrations.png) Press on the Azure Key Vault tile and grant Infisical access to Azure Key Vault. + You can optionally authenticate against a specific tenant by providing the Azure tenant or directory ID. + + ![integrations](/images/integrations/azure-key-vault/integrations-azure-key-vault-tenant-select.png) + Obtain the Vault URI of your key vault in the Overview tab. diff --git a/docs/integrations/cloud/databricks.mdx b/docs/integrations/cloud/databricks.mdx index 7fee3acd3..1971190de 100644 --- a/docs/integrations/cloud/databricks.mdx +++ b/docs/integrations/cloud/databricks.mdx @@ -7,6 +7,12 @@ Prerequisites: - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + + When integrating with Databricks, Infisical is intended to be the source of truth for the secrets in the configured Databricks scope. + + Any secrets not present in Infisical will be removed from the specified scope. To prevent removal of secrets not managed by Infisical, Infisical recommends creating a designated secret scope for your integration. + + Obtain a Personal Access Token in **User Settings** > **Developer** > **Access Tokens**. 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/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx new file mode 100644 index 000000000..5cedd5c3e --- /dev/null +++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx @@ -0,0 +1,466 @@ +--- +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. + + Example: + + ```yaml + spec: + kubernetesAuth: + identityId: + 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..67fc7f2a8 --- /dev/null +++ b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx @@ -0,0 +1,412 @@ +--- +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. + + Example: + + ```yaml + spec: + kubernetesAuth: + identityId: + 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 + ``` + + + + + +## 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.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx similarity index 62% rename from docs/integrations/platforms/kubernetes.mdx rename to docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx index c39041375..3266fd2b0 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx @@ -1,66 +1,11 @@ --- -title: "Kubernetes Operator" -description: "How to use Infisical to inject secrets into Kubernetes clusters." +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" --- -![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). +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 @@ -75,97 +20,28 @@ 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 + 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 }}" ``` -### InfisicalSecret CRD properties +## 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 @@ -217,10 +93,12 @@ When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. 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 - +### 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. @@ -657,24 +535,40 @@ spec: - -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. +### Operator Managed Secrets -Note: The managed secret be should be created in the same namespace as the deployment that will use it. +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. @@ -690,74 +584,271 @@ This is useful for tools such as ArgoCD, where every resource requires an owner -### Propagating labels & annotations +#### Managed Secret Templating -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: +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. - -```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 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"` +} ``` -This would result in the following managed secret to be created: +#### Example template configuration: ```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 +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 + + + **Function name**: decodeBase64ToBytes + +**Description**: +Given a base64 encoded string, this function will decodes the base64-encoded string. +This function is useful when your secrets are already stored as base64 encoded value in Infisical. + +**Returns**: The decoded base64 string as bytes. + +**Example**: +The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a base64 encoded value in Infisical. +The resulting managed secret will contain the decoded value of `BINARY_KEY_BASE64`. + +```yaml +managedKubeSecretReferences: +secretName: managed-secret +secretNamespace: default +template: + includeAllSecrets: true + data: + BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}" ``` -### 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. +### 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 + + + **Function name**: decodeBase64ToBytes + + **Description**: + Given a base64 encoded string, this function will decodes the base64-encoded string. + This function is useful when your Infisical secrets are already stored as base64 encoded value in Infisical. + + **Returns**: The decoded base64 string as bytes. + + **Example**: + The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a base64 encoded value in Infisical. + The resulting managed config map will contain the decoded value of `BINARY_KEY_BASE64`. + + ```yaml + managedKubeConfigMapReferences: + - configMapName: managed-configmap + configMapNamespace: default + template: + includeAllSecrets: true + data: + BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}" + ``` + + + + +## 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 ``` -### 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 -``` + + + ```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. + + + - - 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. +## 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/) @@ -808,7 +899,7 @@ spec: valueFrom: secretKeyRef: name: managed-secret # managed secret name - key: SOME_SECRET_KEY # The name of the key which exists in the managed secret + key: SOME_SECRET_KEY # The name of the key which exists in the managed secret ``` Example usage in a deployment @@ -817,28 +908,30 @@ Example usage in a deployment apiVersion: apps/v1 kind: Deployment metadata: -name: nginx-deployment -labels: -app: nginx + 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 - + 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 ``` @@ -897,25 +990,6 @@ spec: -### 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 @@ -933,14 +1007,14 @@ stringData: -----END CERTIFICATE----- ``` -## Auto redeployment +### 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 auto redeploy +#### Enabling Automatic Redeployment -To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret +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" @@ -981,91 +1055,210 @@ spec: 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. +## Using Managed ConfigMap In Your Deployment -### Available global properties +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/) -| Property | Description | Default value | -| -------- | --------------------------------------------------------------------------------- | ----------------------------- | -| hostAPI | If `hostAPI` in `InfisicalSecret` instance is left empty, this value will be used | https://app.infisical.com/api | + + Automatic redeployment of deployments using managed ConfigMaps is not yet supported. + -### 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. + + This will take all the secrets from your managed ConfigMap and expose them to your container -```yaml infisical-config.yaml -apiVersion: v1 -kind: Namespace +````yaml + envFrom: + - configMapRef: + name: managed-configmap # managed configmap name + ``` + + Example usage in a deployment + ```yaml + apiVersion: apps/v1 +kind: Deployment 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 -``` + 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 +```` -Then apply this change via kubectl by running the following + -```bash -kubectl apply -f infisical-config.yaml -``` + + 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 + ``` -## 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. +Example usage in a deployment ```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: +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 ``` -## Uninstall Operator + -The managed secret created by the operator will not be deleted when the operator is uninstalled. + +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 +```` - - - 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 - ``` - - +You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets -## Useful Articles +```yaml +volumeMounts: + - name: configmaps-volume-name + mountPath: /etc/config + readOnly: true +``` -- [Managing secrets in OpenShift with Infisical](https://xphyr.net/post/infisical_ocp/) +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..c4e3f7c64 --- /dev/null +++ b/docs/integrations/platforms/kubernetes/overview.mdx @@ -0,0 +1,195 @@ +--- +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 +--- +``` + + +## 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/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/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 index 1fce09def..737d3b71e 100644 --- a/docs/internals/permissions.mdx +++ b/docs/internals/permissions.mdx @@ -52,6 +52,7 @@ Refer to the table below for a list of subjects and the actions they support. | `pki-collections` | `read`, `create`, `edit`, `delete` | | `kms` | `edit` | | `cmek` | `read`, `create`, `edit`, `delete`, `encrypt`, `decrypt` | +| `secret-syncs` | `read`, `create`, `edit`, `delete`, `sync-secrets`, `import-secrets`, `remove-secrets` |
@@ -63,21 +64,23 @@ Refer to the table below for a list of subjects and the actions they 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` | +| 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` | +| `project-templates` | `read`, `create`, `edit`, `delete` | +| `app-connections` | `read`, `create`, `edit`, `delete`, `connect` | +| `kms` | `read` |
@@ -90,7 +93,6 @@ Permission inversion allows you to explicitly deny actions instead of allowing t - secret-folders - secret-imports - dynamic-secrets -- cmek When a permission is inverted, it changes from an "allow" rule to a "deny" rule. For example: 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 fae4ade1a..864fba87d 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,12 +118,14 @@ "documentation/platform/pki/alerting" ] }, + "documentation/platform/ssh", { "group": "Key Management (KMS)", "pages": [ "documentation/platform/kms/overview", "documentation/platform/kms/hsm-integration", - "documentation/platform/kms/kubernetes-encryption" + "documentation/platform/kms/kubernetes-encryption", + "documentation/platform/kms/kmip" ] }, { @@ -127,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" ] }, { @@ -143,7 +150,14 @@ "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", @@ -188,10 +202,16 @@ "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", { "group": "Workflow Integrations", @@ -207,7 +227,8 @@ "documentation/platform/admin-panel/org-admin-console" ] }, - "documentation/platform/secret-sharing" + "documentation/platform/secret-sharing", + "documentation/platform/secret-scanning" ] }, { @@ -221,12 +242,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", @@ -242,7 +266,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" ] @@ -272,7 +303,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", @@ -280,19 +311,24 @@ "self-hosting/deployment-options/kubernetes-helm" ] }, + "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/ee", "self-hosting/faq" @@ -310,6 +346,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", @@ -337,7 +377,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" ] @@ -353,7 +402,45 @@ ] }, "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/aws", + "integrations/app-connections/azure-app-configuration", + "integrations/app-connections/azure-key-vault", + "integrations/app-connections/databricks", + "integrations/app-connections/gcp", + "integrations/app-connections/github", + "integrations/app-connections/humanitec" + ] + } + ] + }, + { + "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/databricks", + "integrations/secret-syncs/gcp-secret-manager", + "integrations/secret-syncs/github", + "integrations/secret-syncs/humanitec" + ] + } ] }, { @@ -417,7 +504,8 @@ "integrations/cicd/travisci", "integrations/cicd/rundeck", "integrations/cicd/codefresh", - "integrations/cloud/checkly" + "integrations/cloud/checkly", + "integrations/cicd/octopus-deploy" ] } ] @@ -445,7 +533,8 @@ "integrations/frameworks/laravel", "integrations/frameworks/rails", "integrations/frameworks/dotnet", - "integrations/platforms/pm2" + "integrations/platforms/pm2", + "integrations/frameworks/ab-initio" ] } ] @@ -463,9 +552,9 @@ "pages": [ "sdks/languages/node", "sdks/languages/python", + "sdks/languages/java", "sdks/languages/go", "sdks/languages/ruby", - "sdks/languages/java", "sdks/languages/csharp" ] }, @@ -572,6 +661,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": [ @@ -735,6 +834,213 @@ "api-reference/endpoints/identity-specific-privilege/list" ] }, + { + "group": "App Connections", + "pages": [ + "api-reference/endpoints/app-connections/list", + "api-reference/endpoints/app-connections/options", + { + "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": "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": "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": "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": "Integrations", "pages": [ @@ -825,6 +1131,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": [ @@ -832,6 +1172,8 @@ "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", @@ -847,7 +1189,6 @@ "internals/overview", "internals/permissions", "internals/components", - "internals/flows", "internals/security", "internals/service-tokens" ] @@ -883,9 +1224,6 @@ ] } ], - "integrations": { - "intercom": "hsg644ru" - }, "analytics": { "koala": { "publicApiKey": "pk_b50d7184e0e39ddd5cdb43cf6abeadd9b97d" @@ -902,13 +1240,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..344b0a663 100644 --- a/docs/sdks/languages/csharp.mdx +++ b/docs/sdks/languages/csharp.mdx @@ -42,7 +42,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..a0bf48687 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) @@ -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,10 @@ 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. + @@ -140,6 +144,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 +155,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 +170,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 +190,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 +201,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 +216,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 +226,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 +242,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 +253,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 +269,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") @@ -262,6 +283,7 @@ if err != nil { ## Working With Secrets ### List Secrets + `client.Secrets().List(options)` Retrieve all secrets within the Infisical project and environment that client is connected to. @@ -275,7 +297,7 @@ secrets, err := client.Secrets().List(infisical.ListSecretsOptions{ }) ``` -### Parameters +#### Parameters @@ -311,7 +333,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 +348,34 @@ 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". + + ### + ### Create Secret + `client.Secrets().Create(options)` Create a new secret in Infisical. @@ -363,36 +391,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 +439,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 +490,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. + + + + + + + + +## Working With Folders ### + ### List Folders + `client.Folders().List(options)` Retrieve all within the Infisical project and environment that client is connected to. @@ -490,7 +602,7 @@ folders, err := client.Folders().List(infisical.ListFoldersOptions{ }) ``` -### Parameters +#### Parameters @@ -510,7 +622,9 @@ folders, err := client.Folders().List(infisical.ListFoldersOptions{ ### + ### Create Folder + `client.Folders().Create(options)` Create a new folder in Infisical. @@ -524,28 +638,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 +676,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 +718,7 @@ deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{ }) ``` -### Parameters +#### Parameters @@ -620,6 +739,5 @@ deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{ The path from where the folder should be deleted. + - - 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/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..8eda21edd 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -30,9 +30,31 @@ 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`. +## 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 @@ -65,12 +87,13 @@ 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 +## 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 +373,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 +439,53 @@ 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 +559,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 +588,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/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 1a5abb775..1fa72b81a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,285 +1,207 @@ { - "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", + "@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", "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.11", + "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 +216,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 +272,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 +323,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 +390,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 +407,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 +422,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 +435,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 +466,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 +501,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 +547,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 +562,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 +593,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 +601,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 +620,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 +676,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 +701,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 +721,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 +756,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 +773,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 +790,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 +807,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 +824,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 +841,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 +858,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 +875,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 +892,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 +909,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 +926,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 +943,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 +960,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 +977,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 +994,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 +1010,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 +1044,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 +1079,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 +1096,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 +1113,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 +1130,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 +1147,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 +1178,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 +1286,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 +1370,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 +1388,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 +1405,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 +1434,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 +1444,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 +1470,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 +1482,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 +1498,102 @@ "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/@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": { + "@lottiefiles/dotlottie-web": "0.38.2" }, "peerDependencies": { - "react": ">=16" + "react": "^17 || ^18 || ^19" } }, - "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==", - "dependencies": { - "@motionone/easing": "^10.17.0", - "@motionone/types": "^10.17.0", - "@motionone/utils": "^10.17.0", - "tslib": "^2.3.1" - } - }, - "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==", - "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" - } - }, - "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==", - "dependencies": { - "@motionone/utils": "^10.17.0", - "tslib": "^2.3.1" - } - }, - "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==", - "dependencies": { - "@motionone/types": "^10.17.0", - "@motionone/utils": "^10.17.0", - "tslib": "^2.3.1" - } - }, - "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==", - "dependencies": { - "@motionone/types": "^10.17.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - } - }, - "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, - "dependencies": { - "gunzip-maybe": "^1.4.2", - "pump": "^3.0.0", - "tar-fs": "^2.1.1" - } - }, - "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, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "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, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "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, - "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/@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, - "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" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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 +1607,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 +1617,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 +1626,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 +1807,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 +1816,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 +1831,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 +1845,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 +1857,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 +1873,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 +1885,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 +1896,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 +1909,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 +1917,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 +1941,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 +2004,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 +2032,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 +2055,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 +2085,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 +2115,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 +2141,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 +2156,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 +2171,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 +2207,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 +2222,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 +2249,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 +2278,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 +2293,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 +2318,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 +2349,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 +2367,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 +2390,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 +2430,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 +2467,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 +2499,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 +2523,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 +2547,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 +2570,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 +2594,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 +2626,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 +2657,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 +2700,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 +2718,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 +2747,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 +2777,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 +2811,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 +2845,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 +2860,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 +2878,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 +2896,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 +2911,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 +2926,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 +2944,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 +2962,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 +2985,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 +3439,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 +3460,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 +3477,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 +3494,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 +3511,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 +3528,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 +3545,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 +3562,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 +3579,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 +3596,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 +3613,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 +3630,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 +3659,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 +3968,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 +3978,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 +4057,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 +4290,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 +4328,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 +4425,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 +4441,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 +4450,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 +4458,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 +4552,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 +4574,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 +4587,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 +4598,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 +4619,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 +4665,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 +4713,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 +4761,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 +4780,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 +4795,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 +4835,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 +4873,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 +4882,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 +4909,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 +4927,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 +4947,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 +4962,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 +5023,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 +5036,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,177 +5067,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.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "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.13.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/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" @@ -10807,6 +5112,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" }, @@ -10818,19 +5124,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", @@ -10845,6 +5157,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", @@ -10856,6 +5169,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", @@ -10864,62 +5178,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", @@ -10934,11 +5304,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" @@ -10947,19 +5318,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" }, @@ -10986,66 +5349,68 @@ "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.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "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" @@ -11058,25 +5423,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" } @@ -11086,22 +5442,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", @@ -11115,12 +5465,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", @@ -11140,28 +5492,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" @@ -11182,26 +5530,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", @@ -11214,6 +5575,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -11223,6 +5587,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" }, @@ -11230,211 +5595,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" }, @@ -11445,27 +5695,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" }, @@ -11483,178 +5720,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", @@ -11666,104 +5753,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.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "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" @@ -11772,12 +5777,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", @@ -11793,6 +5801,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" } @@ -11802,21 +5811,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", @@ -11830,6 +5842,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", @@ -11839,19 +5852,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", @@ -11862,215 +5884,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" }, @@ -12081,191 +5950,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" @@ -12276,10 +6160,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", @@ -12293,123 +6181,19 @@ "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==", - "dev": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "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" - } + "license": "MIT" }, "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": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -12422,20 +6206,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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", @@ -12448,64 +6224,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", @@ -12519,152 +6246,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", @@ -12672,34 +6290,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" }, @@ -12707,21 +6322,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", @@ -12732,24 +6332,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" }, @@ -12757,222 +6345,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", @@ -12984,60 +6399,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": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "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" @@ -13046,19 +6426,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", @@ -13070,94 +6437,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" @@ -13167,13 +6508,11 @@ } }, "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==", + "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": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -13183,67 +6522,62 @@ "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-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==", - "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" - } - }, "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" @@ -13254,19 +6588,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" @@ -13276,11 +6612,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" }, @@ -13288,66 +6625,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" }, @@ -13355,48 +6672,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", @@ -13446,6 +6734,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", @@ -13467,6 +6756,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", @@ -13481,52 +6771,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" }, @@ -13539,6 +6799,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", @@ -13550,23 +6811,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" @@ -13576,14 +6840,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" }, @@ -13601,39 +6875,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": { @@ -13641,6 +6919,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" } @@ -13650,6 +6929,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" }, @@ -13657,125 +6937,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" }, @@ -13783,11 +7056,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" }, @@ -13800,6 +7085,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", @@ -13812,69 +7099,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" }, @@ -13882,21 +7137,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" }, @@ -13909,6 +7202,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" }, @@ -13919,35 +7213,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", @@ -13960,35 +7231,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" }, @@ -14001,6 +7262,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" }, @@ -14013,53 +7275,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" } @@ -14069,185 +7321,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.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", - "dev": true, - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "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/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", @@ -14264,6 +7384,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" }, @@ -14271,91 +7392,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" }, @@ -14366,67 +7438,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" }, @@ -14434,139 +7454,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.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "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" @@ -14583,6 +7482,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", @@ -14593,30 +7493,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" }, @@ -14631,15 +7524,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" @@ -14651,106 +7546,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", @@ -14760,20 +7560,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": "*" }, @@ -14783,108 +7575,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", @@ -14892,6 +7614,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -14904,6 +7627,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" } @@ -14913,6 +7637,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", @@ -14931,6 +7656,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" } @@ -14939,21 +7665,38 @@ "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.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "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" @@ -14966,63 +7709,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" @@ -15032,10 +7733,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" }, @@ -15043,72 +7745,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" @@ -15119,6 +7772,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" }, @@ -15126,66 +7780,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" @@ -15194,33 +7810,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" @@ -15230,141 +7844,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" } @@ -15374,6 +7869,7 @@ "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" } @@ -15383,6 +7879,7 @@ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -15391,10 +7888,14 @@ } }, "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" }, @@ -15403,10 +7904,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" }, @@ -15415,12 +7917,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" @@ -15433,6 +7936,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", @@ -15442,33 +7946,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" }, @@ -15476,34 +7969,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", @@ -15514,138 +8121,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", @@ -15655,99 +8167,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", @@ -15762,50 +8192,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", @@ -15824,45 +8241,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" @@ -15874,64 +8270,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" @@ -15940,29 +8295,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" }, @@ -15974,36 +8322,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" @@ -16013,14 +8376,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" @@ -16029,13 +8395,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" }, @@ -16047,12 +8415,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" @@ -16063,6 +8435,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" }, @@ -16071,13 +8444,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" @@ -16086,26 +8460,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": { @@ -16113,6 +8475,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" }, @@ -16120,24 +8483,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" @@ -16148,6 +8521,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" }, @@ -16158,33 +8532,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": { @@ -16192,17 +8546,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" @@ -16212,7 +8571,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" } @@ -16222,6 +8581,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" }, @@ -16237,6 +8597,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" }, @@ -16244,45 +8605,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" } @@ -16292,6 +8632,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" @@ -16304,10 +8645,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" }, @@ -16320,17 +8662,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" @@ -16339,20 +8684,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" } @@ -16368,22 +8705,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" @@ -16393,45 +8725,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" @@ -16441,12 +8771,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" @@ -16456,12 +8789,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" @@ -16470,29 +8804,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" } @@ -16502,6 +8822,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" }, @@ -16510,113 +8831,74 @@ } }, "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, - "engines": { - "node": ">=0.10.0" - } + "license": "ISC" }, - "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==", + "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": ">=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": ">=10" } }, "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" }, @@ -16624,148 +8906,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" } @@ -16773,15 +8919,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" @@ -16790,148 +8938,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", @@ -16950,31 +9011,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", @@ -16986,6 +9033,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", @@ -16997,19 +9046,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": { @@ -17017,47 +9059,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" }, @@ -17065,149 +9086,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" @@ -17217,94 +9101,30 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "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": ">=10" + "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" }, @@ -17315,121 +9135,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" }, @@ -17437,213 +9177,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", @@ -17651,32 +9309,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", @@ -17684,94 +9349,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.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "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", @@ -17786,26 +9394,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", @@ -17818,27 +9426,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", @@ -17850,15 +9458,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", @@ -17870,16 +9478,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", @@ -17891,14 +9499,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", @@ -17910,16 +9518,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", @@ -17931,16 +9539,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", @@ -17952,14 +9560,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", @@ -17971,13 +9579,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", @@ -17989,15 +9597,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", @@ -18009,14 +9617,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", @@ -18028,13 +9636,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", @@ -18047,15 +9655,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", @@ -18068,9 +9676,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", @@ -18083,9 +9691,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", @@ -18097,13 +9705,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", @@ -18115,13 +9723,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", @@ -18133,15 +9741,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", @@ -18153,16 +9761,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", @@ -18175,9 +9783,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", @@ -18194,6 +9802,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" @@ -18202,11 +9811,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" @@ -18216,27 +9839,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" } @@ -18245,6 +9858,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" }, @@ -18252,53 +9866,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" }, @@ -18310,129 +9897,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" }, @@ -18440,256 +9962,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" }, @@ -18705,128 +9989,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" } @@ -18836,197 +10057,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" } @@ -19036,27 +10082,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" @@ -19070,6 +10122,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" } @@ -19079,6 +10132,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", @@ -19093,28 +10147,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" @@ -19124,39 +10181,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" @@ -19165,136 +10213,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" }, @@ -19310,6 +10269,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" }, @@ -19320,50 +10280,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" }, @@ -19372,22 +10316,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", @@ -19401,51 +10388,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" } @@ -19455,6 +10420,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" } @@ -19464,6 +10430,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" } @@ -19471,58 +10438,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.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", - "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", @@ -19534,92 +10484,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" @@ -19630,6 +10514,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" } @@ -19639,6 +10524,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" } @@ -19648,6 +10534,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" }, @@ -19655,56 +10542,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", @@ -19719,27 +10580,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" @@ -19750,6 +10613,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" }, @@ -19765,20 +10629,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", @@ -19793,197 +10664,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" @@ -19995,119 +10707,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" @@ -20118,6 +10770,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" }, @@ -20126,29 +10779,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": { @@ -20160,10 +10816,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": { @@ -20178,91 +10834,35 @@ "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, - "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": { "version": "0.11.10", "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" } @@ -20270,97 +10870,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", @@ -20371,190 +10916,63 @@ } }, "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/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "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.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "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" }, @@ -20565,31 +10983,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", @@ -20599,21 +10992,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", @@ -20632,54 +11010,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" } @@ -20689,128 +11037,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", @@ -20825,6 +11074,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", @@ -20838,6 +11088,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", @@ -20852,168 +11103,93 @@ "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", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "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-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": { "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" }, "peerDependencies": { - "react": "17.0.2" - } - }, - "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==", - "dependencies": { - "clsx": "^1.1.1", - "prop-types": "^15.8.1" - }, - "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" - }, - "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" - } - }, - "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-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==", - "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" - }, - "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": { @@ -21026,114 +11202,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", @@ -21153,9 +11268,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" @@ -21173,22 +11289,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", @@ -21199,23 +11303,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", @@ -21234,36 +11342,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": { @@ -21287,138 +11376,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" }, @@ -21426,64 +11408,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" @@ -21492,53 +11456,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" @@ -21547,113 +11481,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", @@ -21661,13 +11496,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", @@ -21675,214 +11511,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", @@ -21896,12 +11559,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": { @@ -21909,82 +11572,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" @@ -21994,7 +11602,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" }, @@ -22005,35 +11615,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", @@ -22053,39 +11683,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": { @@ -22112,16 +11725,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": { @@ -22131,199 +11746,40 @@ "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.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "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/send/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/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.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dev": true, - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.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.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -22337,14 +11793,16 @@ } }, "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" @@ -22354,18 +11812,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" @@ -22374,97 +11828,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" }, @@ -22477,20 +11846,79 @@ "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.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "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.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "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" @@ -22500,135 +11928,33 @@ } }, "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" } @@ -22642,204 +11968,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", @@ -22847,67 +12009,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": { @@ -22916,6 +12042,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", @@ -22929,43 +12056,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" @@ -22975,38 +12167,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" }, @@ -23020,6 +12234,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" }, @@ -23032,47 +12247,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" }, @@ -23080,133 +12265,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" }, @@ -23218,6 +12366,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" }, @@ -23229,88 +12378,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": { @@ -23318,299 +12454,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" @@ -23620,45 +12473,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": { @@ -23666,6 +12504,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" }, @@ -23674,46 +12513,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" }, @@ -23721,54 +12537,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", @@ -23780,30 +12553,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": { @@ -23812,71 +12603,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" }, @@ -23887,41 +13088,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" }, @@ -23930,54 +13124,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" @@ -23987,16 +13172,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" @@ -24006,60 +13194,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": { @@ -24067,6 +13256,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", @@ -24078,96 +13268,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", @@ -24175,11 +13305,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", @@ -24187,11 +13317,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", @@ -24199,13 +13329,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", @@ -24213,12 +13343,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", @@ -24226,71 +13356,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", @@ -24305,9 +13394,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" @@ -24321,40 +13411,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" }, @@ -24372,12 +13459,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": { @@ -24385,31 +13472,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" @@ -24428,11 +13495,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": { @@ -24440,6 +13508,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", @@ -24451,127 +13520,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", @@ -24579,252 +13574,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.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "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" @@ -24835,6 +13767,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" }, @@ -24846,39 +13779,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" @@ -24888,31 +13827,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" @@ -24921,17 +13845,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", @@ -24950,6 +13905,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", @@ -24962,11 +13918,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" }, @@ -24979,6 +13958,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" }, @@ -24986,28 +13966,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" }, @@ -25021,72 +13985,150 @@ "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/yocto-queue": { @@ -25094,6 +14136,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" }, @@ -25101,45 +14144,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": { @@ -25150,8 +14176,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..95ad59c7d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,163 +1,140 @@ { + "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", + "@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", "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.11", + "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 1a536e522..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/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/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/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/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/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/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/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx similarity index 83% rename from frontend/src/views/SecretMainPage/SecretMainPage.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 31635884c..e6fc5eadf 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -1,12 +1,12 @@ import { useCallback, useEffect, useMemo, useState } from "react"; +import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; -import { useRouter } from "next/router"; 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 NavHeader from "@app/components/navigation/NavHeader"; import { createNotification } from "@app/components/notifications"; import { PermissionDeniedBanner } from "@app/components/permissions"; import { @@ -17,6 +17,7 @@ import { Pagination, Tooltip } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, ProjectPermissionDynamicSecretActions, @@ -24,6 +25,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useGetImportedSecretsSingleEnv, @@ -35,16 +37,18 @@ import { import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard"; import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; -import { DynamicSecretListView } from "@app/views/SecretMainPage/components/DynamicSecretListView"; -import { FolderListView } from "@app/views/SecretMainPage/components/FolderListView"; -import { SecretImportListView } from "@app/views/SecretMainPage/components/SecretImportListView"; -import { SecretTableResourceCount } from "@app/views/SecretOverviewPage/components/SecretTableResourceCount/SecretTableResourceCount"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; -import { SecretV2MigrationSection } from "../SecretOverviewPage/components/SecretV2MigrationSection"; +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 { @@ -63,10 +67,19 @@ const LOADER_TEXT = [ "Getting secret import links..." ]; -const SecretMainPageContent = () => { - const { t } = useTranslation(); - const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace(); - const router = useRouter(); +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); @@ -88,18 +101,30 @@ const SecretMainPageContent = () => { const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["snapshots"] as const); // env slug - const environment = router.query.env as string; const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || ""; - const secretPath = (router.query.secretPath as string) || "/"; - const canReadSecret = permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { + 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( @@ -119,7 +144,7 @@ const SecretMainPageContent = () => { const defaultFilterState = { tags: {}, - searchFilter: (router.query.searchFilter as string) || "", + 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, @@ -137,22 +162,23 @@ const SecretMainPageContent = () => { const { togglePopUp } = usePopUpAction(); useEffect(() => { - if ( - !isWorkspaceLoading && - !currentWorkspace?.environments.find((env) => env.slug === environment) && - router.isReady - ) { - router.push(`/project/${workspaceId}/secrets/overview`); + 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 + } + }); } - }, [isWorkspaceLoading, currentWorkspace, environment, router.isReady]); + }, [currentWorkspace, environment]); const { data, - isLoading: isDetailsLoading, + isPending: isDetailsLoading, isFetching: isDetailsFetching } = useGetProjectSecretsDetails({ environment, @@ -165,6 +191,7 @@ const SecretMainPageContent = () => { orderDirection, includeImports: canReadSecretImports && filter.include.import, includeFolders: filter.include.folder, + viewSecretValue: canReadSecretValue, includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, includeSecrets: canReadSecret && filter.include.secret, tags: filter.tags @@ -223,7 +250,7 @@ const SecretMainPageContent = () => { limit: 10 }); - const { data: snapshotCount, isLoading: isSnapshotCountLoading } = useGetWsSnapshotCount({ + const { data: snapshotCount, isPending: isSnapshotCountLoading } = useGetWsSnapshotCount({ workspaceId, environment, directory: secretPath, @@ -251,15 +278,6 @@ const SecretMainPageContent = () => { state === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC ); - const handleEnvChange = (slug: string) => { - const query: Record = { ...router.query, env: slug }; - delete query.secretPath; - router.push({ - pathname: router.pathname, - query - }); - }; - const handleTagToggle = useCallback( (tagSlug: string) => setFilter((state) => { @@ -308,23 +326,13 @@ const SecretMainPageContent = () => { const restore = filterHistory.get(secretPath); setFilter(restore ?? defaultFilterState); setDebouncedSearchFilter(restore?.searchFilter ?? ""); - const { searchFilter, ...query } = router.query; - - // this is a temp work around until we fully transition state to query params, - // setting the initial search filter by query and then moving it to internal state - if (router.query.searchFilter) { - router.push({ - pathname: router.pathname, - query - }); - } }, [secretPath]); useEffect(() => { - if (!router.query.search && !router.query.tags) return; + if (!routerQueryParams.search && !routerQueryParams.tags) return; - const queryTags = router.query.tags - ? (router.query.tags as string).split(",").filter((tag) => Boolean(tag.trim())) + const queryTags = routerQueryParams.tags + ? (routerQueryParams.tags as string).split(",").filter((tag) => Boolean(tag.trim())) : []; const updatedTags: Record = {}; queryTags.forEach((tag) => { @@ -334,17 +342,18 @@ const SecretMainPageContent = () => { setFilter((prev) => ({ ...prev, ...defaultFilterState, - searchFilter: (router.query.search as string) ?? "", + searchFilter: (routerQueryParams.search as string) ?? "", tags: updatedTags })); - setDebouncedSearchFilter(router.query.search as string); + setDebouncedSearchFilter(routerQueryParams.search as string); // this is a temp workaround until we fully transition state to query params, - const { search, tags: qTags, ...query } = router.query; - router.push({ - pathname: router.pathname, - query + navigate({ + search: (state) => { + const { search, tags: qTags, ...query } = state; + return query; + } }); - }, [router.query.search, router.query.tags]); + }, [routerQueryParams.search, routerQueryParams.tags]); const selectedSecrets = useSelectedSecrets(); const selectedSecretActions = useSelectedSecretActions(); @@ -389,21 +398,8 @@ const SecretMainPageContent = () => { setDebouncedSearchFilter(""); }; return ( -
+
-
- -
{!isRollbackMode ? ( <> { isSnapshotCountLoading={isSnapshotCountLoading} onToggleRowType={handleToggleRowType} onClickRollbackMode={() => handlePopUpToggle("snapshots", true)} + protectedBranchPolicyName={boardPolicy?.name} />
@@ -552,7 +549,6 @@ const SecretMainPageContent = () => { { ); }; -export const SecretMainPage = () => ( - - - -); +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 100% rename from frontend/src/views/SecretMainPage/SecretMainPage.types.ts rename to frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.types.ts diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx similarity index 85% rename from frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 0df6f82c2..681ad99e3 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -15,14 +15,17 @@ import { faFolder, faFolderPlus, faKey, + faLock, faMinusSquare, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +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 { @@ -40,8 +43,7 @@ import { IconButton, Modal, ModalContent, - Tooltip, - UpgradePlanModal + Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, @@ -53,8 +55,8 @@ import { import { usePopUp } from "@app/hooks"; import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; -import { SecretType, WsTag } from "@app/hooks/api/types"; -import { SecretSearchInput } from "@app/views/SecretOverviewPage/components/SecretSearchInput"; +import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types"; +import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput"; import { PopUpNames, @@ -80,6 +82,7 @@ type Props = { isVisible?: boolean; snapshotCount: number; isSnapshotCountLoading?: boolean; + protectedBranchPolicyName?: string; onSearchChange: (term: string) => void; onToggleTagFilter: (tagId: string) => void; onVisibilityToggle: () => void; @@ -101,7 +104,8 @@ export const ActionBar = ({ onToggleTagFilter, onVisibilityToggle, onClickRollbackMode, - onToggleRowType + onToggleRowType, + protectedBranchPolicyName }: Props) => { const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ "addFolder", @@ -112,9 +116,9 @@ export const ActionBar = ({ "misc", "upgradePlan" ] as const); + const isProtectedBranch = Boolean(protectedBranchPolicyName); const { subscription } = useSubscription(); const { openPopUp } = usePopUpAction(); - const { mutateAsync: createFolder } = useCreateFolder(); const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch(); const { mutateAsync: moveSecrets } = useMoveSecrets(); @@ -125,13 +129,14 @@ export const ActionBar = ({ const { currentWorkspace } = useWorkspace(); - const handleFolderCreate = async (folderName: string) => { + const handleFolderCreate = async (folderName: string, description: string | null) => { try { await createFolder({ name: folderName, path: secretPath, environment, - projectId: workspaceId + projectId: workspaceId, + description }); handlePopUpClose("addFolder"); createNotification({ @@ -148,51 +153,71 @@ export const ActionBar = ({ }; const handleSecretDownload = async () => { - 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 + 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 - }); + 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); } - 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 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`); }; const handleSecretBulkDelete = async () => { @@ -276,7 +301,7 @@ export const ActionBar = ({ className="w-2/5" value={filter.searchFilter} onChange={onSearchChange} - environments={[currentWorkspace?.environments.find((env) => env.slug === environment)!]} + environments={[currentWorkspace.environments.find((env) => env.slug === environment)!]} projectId={workspaceId} tags={tags} /> @@ -307,7 +332,7 @@ export const ActionBar = ({ iconPos="right" >
- + Imports
@@ -335,7 +360,7 @@ export const ActionBar = ({ iconPos="right" >
- + Dynamic Secrets
@@ -348,7 +373,7 @@ export const ActionBar = ({ iconPos="right" >
- + Secrets
@@ -387,6 +412,15 @@ export const ActionBar = ({
+
+ {isProtectedBranch && ( + + + + + + )} +
@@ -547,7 +581,7 @@ export const ActionBar = ({ isMultiSelectActive && "h-16" )} > -
+
@@ -621,8 +655,9 @@ export const ActionBar = ({ isOpen={popUp.addDynamicSecret.isOpen} onToggle={(isOpen) => handlePopUpToggle("addDynamicSecret", isOpen)} projectSlug={projectSlug} - environment={environment} + environments={[{ slug: environment, name: environment, id: "not-used" }]} secretPath={secretPath} + isSingleEnvironmentMode /> 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/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx similarity index 60% rename from frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx index 6d92641c6..e1ebebc6f 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx @@ -11,28 +11,24 @@ import { AccordionItem, AccordionTrigger, Button, + FilterableSelect, FormControl, Input, - SecretInput, - Select, - SelectItem, TextArea } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; -import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ - client: z.nativeEnum(SqlProviders), 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), - renewStatement: z.string().optional(), - ca: z.string().optional() + revocationStatement: z.string().min(1) }), defaultTTL: z.string().superRefine((val, ctx) => { const valMs = ms(val); @@ -54,7 +50,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; @@ -63,96 +60,61 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; }; -const getSqlStatements = (provider: SqlProviders) => { - if (provider === SqlProviders.MySql) { - return { - creationStatement: - "CREATE USER \"{{username}}\"@'%' IDENTIFIED BY '{{password}}';\nGRANT ALL ON \"{{database}}\".* TO \"{{username}}\"@'%';", - renewStatement: "", - revocationStatement: - 'REVOKE ALL PRIVILEGES ON "{{database}}".* FROM "{{username}}"@\'%\';\nDROP USER "{{username}}"@\'%\';' - }; - } - - if (provider === SqlProviders.Oracle) { - return { - creationStatement: - 'CREATE USER "{{username}}" IDENTIFIED BY "{{password}}";\nGRANT CONNECT TO "{{username}}";\nGRANT CREATE SESSION TO "{{username}}";', - renewStatement: "", - revocationStatement: - 'REVOKE CONNECT FROM "{{username}}";\nREVOKE CREATE SESSION FROM "{{username}}";\nDROP USER "{{username}}";' - }; - } - if (provider === SqlProviders.MsSQL) { - return { - creationStatement: - "CREATE LOGIN [{{username}}] WITH PASSWORD = '{{password}}';\nCREATE USER [{{username}}] FOR LOGIN [{{username}}];\nGRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [{{username}}];", - renewStatement: "", - revocationStatement: "DROP USER [{{username}}];\nDROP LOGIN [{{username}}];" - }; - } - - return { - creationStatement: - "CREATE USER \"{{username}}\" WITH ENCRYPTED PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{username}}\";", - renewStatement: "ALTER ROLE \"{{username}}\" VALID UNTIL '{{expiration}}';", - revocationStatement: - 'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{username}}";\nDROP ROLE "{{username}}";' - }; -}; - -const getDefaultPort = (provider: SqlProviders) => { - switch (provider) { - case SqlProviders.MySql: - return 3306; - case SqlProviders.Oracle: - return 1521; - case SqlProviders.MsSQL: - return 1433; - default: - return 5432; - } -}; - -export const SqlDatabaseInputForm = ({ +export const SapAseInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, - projectSlug + projectSlug, + isSingleEnvironmentMode }: Props) => { const { control, - setValue, formState: { isSubmitting }, handleSubmit } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - provider: getSqlStatements(SqlProviders.Postgres) + 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 }: 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.SqlDatabase, inputs: provider }, + provider: { type: DynamicSecretProviders.SapAse, inputs: provider }, maxTTL, name, path: secretPath, defaultTTL, projectSlug, - environmentSlug: environment + environmentSlug: environment.slug }); onCompleted(); - } catch (err) { + } catch { createNotification({ type: "error", text: "Failed to create dynamic secret" @@ -160,14 +122,6 @@ export const SqlDatabaseInputForm = ({ } }; - const handleDatabaseChange = (type: SqlProviders) => { - const sqlStatment = getSqlStatements(type); - setValue("provider.creationStatement", sqlStatment.creationStatement); - setValue("provider.renewStatement", sqlStatment.renewStatement); - setValue("provider.revocationStatement", sqlStatment.revocationStatement); - setValue("provider.port", getDefaultPort(type)); - }; - return (

@@ -227,29 +181,6 @@ export const SqlDatabaseInputForm = ({ Configuration
-
Service
- ( - - - - )} - />
- + )} /> ( )} /> + ( + + + + )} + />
-
+
( ( )} /> - ( - - - - )} - />
- ( - - - - )} - /> Modify SQL Statements @@ -354,7 +270,7 @@ export const SqlDatabaseInputForm = ({ label="Creation Statement" isError={Boolean(error?.message)} errorText={error?.message} - helperText="username, password and expiration are dynamically provisioned" + helperText="username and password are dynamically provisioned. The sp_addlogin statement is automatically called against the master database. All other statements are called against the database you specify." >